├── .gitignore
├── LICENSE
├── README.md
├── bin
└── reload.sh
├── components
├── Alert.js
├── AppExport
│ ├── AdvancedConfig.js
│ ├── ExportApps.js
│ ├── GenericExport.js
│ └── InputComponents.js
├── AppIcon.js
├── CreatePackForm.js
├── DonateCard.js
├── Error.js
├── FeaturePromoter.js
├── Footer.js
├── ListPackages.js
├── ListSort.js
├── MetaTags.js
├── Nav.js
├── PackAppsList.js
├── PackPreview.js
├── PageWrapper.js
├── PopularApps.js
├── PrettyApp.js
├── RecentApps.js
├── Recommendations.js
├── Search.js
├── SelectionBar.js
└── SingleApp.js
├── ctx
├── PopularContext.js
└── SelectedContext.js
├── data
└── popularApps.json
├── next.config.js
├── package.json
├── pages
├── 404.js
├── _app.js
├── _document.js
├── api
│ ├── auth
│ │ └── [...nextauth].js
│ └── twitter.js
├── apps.js
├── apps
│ └── [id].js
├── eli5.js
├── generate.js
├── index.js
├── packs
│ ├── [id].js
│ ├── create.js
│ ├── edit.js
│ └── index.js
├── privacy.js
├── sitemap.xml.js
└── users
│ ├── [id].js
│ └── you.js
├── public
├── assets
│ ├── apps
│ │ ├── chrome.webp
│ │ ├── code-insiders.webp
│ │ ├── code.webp
│ │ ├── discord.webp
│ │ ├── dropbox.webp
│ │ ├── edge.webp
│ │ ├── fallback
│ │ │ ├── chrome.png
│ │ │ ├── code-insiders.png
│ │ │ ├── code.png
│ │ │ ├── discord.png
│ │ │ ├── dropbox.png
│ │ │ ├── edge.png
│ │ │ ├── firefox.png
│ │ │ ├── node.png
│ │ │ ├── notion.png
│ │ │ ├── npp.png
│ │ │ ├── obs.png
│ │ │ ├── python.png
│ │ │ ├── sharex.png
│ │ │ ├── spotify.png
│ │ │ ├── teams.png
│ │ │ ├── terminal.png
│ │ │ ├── trumpet.png
│ │ │ ├── tweeten.png
│ │ │ ├── whatsapp.png
│ │ │ └── zoom.png
│ │ ├── firefox.webp
│ │ ├── node.webp
│ │ ├── notion.webp
│ │ ├── npp.webp
│ │ ├── obs.webp
│ │ ├── python.webp
│ │ ├── sharex.webp
│ │ ├── spotify.webp
│ │ ├── teams.webp
│ │ ├── terminal.webp
│ │ ├── trumpet.webp
│ │ ├── tweeten.webp
│ │ ├── whatsapp.webp
│ │ └── zoom.webp
│ ├── clippy.png
│ ├── dl.svg
│ ├── hero.png
│ ├── logo.svg
│ ├── packsPromo.svg
│ └── winget-pro.svg
├── cover.png
├── favicon.ico
├── generic-app-icon.svg
├── logo192.png
├── logo512.png
├── manifest.json
├── robots.txt
├── vercel-dark.svg
└── vercel.svg
├── styles
├── alert.module.scss
├── appList.module.scss
├── apps.module.scss
├── base.scss
├── create.module.scss
├── donateCard.module.scss
├── error.module.scss
├── exportApps.module.scss
├── featurePromoter.module.scss
├── footer.module.scss
├── home.module.scss
├── listSort.module.scss
├── nav.module.scss
├── packPage.module.scss
├── packPreview.module.scss
├── packsAppList.module.scss
├── prettyApp.module.scss
├── recommendations.module.scss
├── search.module.scss
└── singleApp.module.scss
├── utils
├── fetchWinstallAPI.js
├── generateWingetImport.js
└── helpers.js
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 | .next
25 | diff.js
26 |
27 | .env
28 |
29 | **/public/workbox-*.js
30 | **/public/sw.js
31 | **/public/workbox-*.js.map
32 | **/public/sw.js.map
33 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # winstall
2 |
3 | ## A web app for browsing Windows Package Manager apps, and create a batch-installation command using an intuitive interface.
4 |
5 | How does it work?
6 | -----------------------
7 |
8 | winstall is powered by Windows Package Manager (aka "winget"), [Microsoft's new package manager](https://devblogs.microsoft.com/commandline/windows-package-manager-preview/) for Windows 10 and 11. it is available by default in Windows 10 and 11.
9 |
10 | Installing Windows Package Manager
11 | ----------------------------------
12 |
13 | If you don't already have Windows Package Manager, you can install it by downloading and installing the [latest .appxbundle file from here.](https://github.com/microsoft/winget-cli/releases)
14 |
15 | Using winstall
16 | --------------
17 |
18 | To use winstall, you can search for apps on the homepage. Additionally, you can also view all the apps available via Windows Package Manager [on this page.](https://winstall.app//apps)
19 |
20 | Simply select the apps you want to download and click on the "Generate Script" button at the bottom of the screen. You will then be presented with a command that you can copy and paste into any Windows command-line. Input that into a command line app of your choice, and hit enter to start installing the apps one-by-one using Windows Package Manager. You can also generate a PowerShell script by toggling the "Show Powershell scirpt" option.
21 |
22 | Alternatively, you can click on the "Download .bat/.ps1" button which will download a batch file. However, you will likely get a security warning from your browser. In that case, ignore the warning as the batch file is completely secure. Once downloaded, you can double-click the .bat/ file to install the apps using the Windows Package Manager.
23 |
24 | How is the data obtained?
25 | -------------------------
26 |
27 | winstall is powered by an API that I have built. The API regularly checks Microsoft's [official repository](https://github.com/microsoft/winget-pkgs) for Windows Package Manager apps. This means it always provides the latest data.
28 |
29 | The API updates its data every 15 minutes on weekdays, and every 3 hours on weekends. I will be making the API open-source in the near-future.
30 |
31 | Popular apps
32 | ------------
33 |
34 | The list of popular apps are fetched from a `.json` file with a pre-populated set of data. On the front-end, a random selection of 6 apps from the list is displayed. If you would like to add an app to the list of popular apps, you can do so by [adding an app here](https://github.com/omaha-consulting/winstall/blob/master/data/popularApps.json) and creating a pull request. You will also have to provide a logo for that app, which needs to have a transparent image, be 80x80px, and in the .webp format. The logo must be [added in this folder.](https://github.com/omaha-consulting/winstall/tree/master/public/assets/apps). And because Safari does not like .webp, you need to also add a .png version of the same image under /apps/fallback.
35 |
36 | History
37 | ------------
38 | winstall was originally created by [Mehedi Hassan](https://github.com/MehediH) as a side project, but is now owned and maintained by [winget.Pro](https://winget.pro) - with [winget.Pro](https://winget.pro), you can have your own, securely hosted repositories for the Windows Package Manager.
39 |
--------------------------------------------------------------------------------
/bin/reload.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | source ~/.bashrc
4 |
5 | cd /srv
6 | npm run build
7 | pm2 delete all
8 | pm2 start npm --name "next-app" -- start
9 | pm2 save
--------------------------------------------------------------------------------
/components/Alert.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { FiX } from "react-icons/fi";
3 | import styles from "../styles/alert.module.scss";
4 |
5 | const Alert = ({ text, id, children, accent=false }) => {
6 | const [hidden, setHidden] = useState(true);
7 |
8 | useEffect(() => {
9 | const isHidden = localStorage.getItem(`alert-${id}`);
10 |
11 | if(!isHidden) setHidden(false);
12 | })
13 |
14 | const handleHide = () => {
15 | localStorage.setItem(`alert-${id}`, true);
16 | setHidden(true);
17 | }
18 |
19 | if (hidden) return null;
20 |
21 | return (
22 |
23 | {children}
24 |
{text}
25 |
26 |
27 | )
28 | }
29 |
30 | export default Alert;
--------------------------------------------------------------------------------
/components/AppExport/AdvancedConfig.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import { FiChevronDown, FiInfo } from "react-icons/fi";
3 | import styles from "../../styles/exportApps.module.scss";
4 | import { CheckboxConfig, RadioConfig, TextInputConfig } from "./InputComponents";
5 |
6 | const AdvancedConfig = ({ refreshConfig, activeTab }) => {
7 | const [ expanded, setExpnaded ] = useState(false);
8 |
9 | const [ config, setConfig ] = useState({
10 | "--scope": null,
11 | "-i": false,
12 | "-h": false,
13 | "-o": "",
14 | "--override": false,
15 | "-l": "",
16 | "--force": false,
17 | "--ignore-unavailable": false
18 | });
19 |
20 | const [ hiddenOptions, setHiddenOptions ] = useState([]);
21 |
22 | const updateConfig = async (key, val) => {
23 | const existingConfig = { ...config };
24 | const newConfig = { ...existingConfig, [key]: val};
25 |
26 | setConfig(newConfig);
27 | refreshConfig(newConfig, hiddenOptions);
28 |
29 | await localStorage.setItem("winstall-advanced-config", JSON.stringify(newConfig));
30 | }
31 |
32 | useEffect(() => {
33 | const loadExistingConfig = async (unavailableOptions) => {
34 | let previousConfig = await localStorage.getItem("winstall-advanced-config")
35 |
36 | if(previousConfig){
37 | previousConfig = JSON.parse(previousConfig);
38 | setExpnaded(true);
39 | } else{
40 | previousConfig = { ...config };
41 | }
42 |
43 | refreshConfig(previousConfig, unavailableOptions);
44 | setConfig(previousConfig);
45 | }
46 |
47 |
48 | let unavailableOptions = ["--ignore-unavailable"];
49 |
50 | if(activeTab === ".json"){
51 | unavailableOptions = ["--scope", "-i", "-h", "-o", "--override", "-l", "--force"];
52 | }
53 |
54 | setHiddenOptions(unavailableOptions);
55 | loadExistingConfig(unavailableOptions);
56 |
57 | }, [ activeTab ]);
58 |
59 |
60 | return (
61 |
62 |
setExpnaded(e => !e)}>
63 |
64 | Advanced Options
65 |
66 |
67 | { expanded && (
68 |
69 |
All of the following options are persisted locally in your browser.
70 |
71 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 | )}
92 |
93 | )
94 | }
95 |
96 | export default AdvancedConfig;
--------------------------------------------------------------------------------
/components/AppExport/ExportApps.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useMemo } from "react";
2 | import styles from "../../styles/exportApps.module.scss";
3 | import generateWingetImport from "../../utils/generateWingetImport";
4 | import GenericExport from "./GenericExport";
5 | import AdvancedConfig from "./AdvancedConfig";
6 |
7 | const ExportApps = ({ apps, title, subtitle }) => {
8 | const [ batScript, setBatScript ] = useState("");
9 | const [ psScript, setPsScript ] = useState("");
10 | const [ wingetScript, setWingetScript ] = useState("");
11 | const [ filters, setFilters ] = useState({});
12 | const [ wingetImportCommand, setWingetImportCommand ] = useState("");
13 | const [active, setActive] = useState(".bat");
14 |
15 | const tabs = useMemo(() => {
16 | return [
17 | {
18 | title: "Batch",
19 | key: ".bat",
20 | element:
21 | },
22 | {
23 | title: "PowerShell",
24 | key: ".ps1",
25 | element:
26 | },
27 | {
28 | title: "Winget Import",
29 | key: ".json",
30 | element:
37 | }
38 | ]
39 | }, [ batScript, psScript, wingetScript, wingetImportCommand, apps ])
40 |
41 |
42 | let handleScriptChange = async () => {
43 | if(!apps) return;
44 |
45 | let installs = [];
46 |
47 | let advancedFilters = "";
48 |
49 | if(filters){
50 | advancedFilters = Object.entries({ ...filters} ).filter(i => i[1] === true).map(i => i[0]).join(" ");
51 |
52 | if(filters["-o"]) advancedFilters += ` -o "${filters["-o"]}"`;
53 | if(filters["-l"]) advancedFilters += ` -l "${filters["-l"]}"`;
54 | if(filters["--scope"]) advancedFilters += ` --scope "${filters["--scope"]}"`;
55 | }
56 |
57 | apps.map((app) => {
58 | installs.push(
59 | `winget install --id=${app._id}${app.selectedVersion !== app.latestVersion ? ` -v "${app.selectedVersion}"` : ""} -e ${advancedFilters}`
60 | );
61 |
62 | return app;
63 | });
64 |
65 | let newBatchScript = installs.join(" && ");
66 | let newPSScript = installs.join(" ; ");
67 |
68 |
69 | setBatScript(newBatchScript);
70 | setPsScript(newPSScript);
71 | setWingetScript(JSON.stringify(await generateWingetImport(apps), 2));
72 |
73 | setWingetImportCommand(`winget import --import-file "$fileName" ${advancedFilters}`);
74 |
75 | };
76 |
77 | useEffect(() => {
78 | const restoreDefaultTab = async () => {
79 | const defaultExportTab = await localStorage.getItem("winstall-default-export-tab");
80 |
81 | if(defaultExportTab !== null){
82 | setActive(defaultExportTab);
83 | }
84 | }
85 |
86 | restoreDefaultTab();
87 | handleScriptChange();
88 | }, [handleScriptChange]);
89 |
90 | const changeTab = async ( tabKey ) => {
91 | setActive(tabKey);
92 | await localStorage.setItem("winstall-default-export-tab", tabKey);
93 | }
94 |
95 | const refreshFilters = async ( newConfig, unavailableOptions ) => {
96 | let availableConfig = { ...newConfig }
97 |
98 | await unavailableOptions.map(opt => {
99 | delete availableConfig[opt]
100 | });
101 |
102 | setFilters(availableConfig);
103 | }
104 |
105 | return (
106 |
107 |
108 | { title && (
109 |
110 |
{title}
111 |
112 | )}
113 |
114 | { subtitle &&
{subtitle}
}
115 |
116 |
117 | { tabs.map((tab, index) => {
118 | return changeTab(tab.key)}>{tab.title}
119 | }) }
120 |
121 |
122 |
123 |
124 | { tabs.map((tab, index) => {
125 | return
126 | }) }
127 |
128 |
129 | )
130 | }
131 |
132 | export default ExportApps;
--------------------------------------------------------------------------------
/components/AppExport/GenericExport.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import { FiCopy, FiDownload, FiInfo } from "react-icons/fi";
3 | import styles from "../../styles/exportApps.module.scss";
4 |
5 | let handleCopy = ( fileContent, setCopyText ) => {
6 | navigator.clipboard.writeText(fileContent).then(() => {
7 | if(setCopyText) setCopyText("Copied!")
8 | }).catch(() => {
9 | document.querySelector("textarea").select();
10 | })
11 | }
12 |
13 | let handleDownload = ( fileContent, fileExtension, downloadId ) => {
14 | let dl = document.querySelector("#gsc");
15 | dl.setAttribute("download", `winstall-${downloadId}${fileExtension}`)
16 | dl.href = "data:text/plain;base64," + btoa(fileContent);
17 | dl.click();
18 | }
19 |
20 | const GenericExport = ({ fileContent, displayedCommand, fileExtension, prioritiseDownload=false, tip }) => {
21 | const [copyText, setCopyText] = useState("Copy to clipboard");
22 | const [textboxContent, setTextboxContent] = useState();
23 | const [downloadId, setDownloadId] = useState(Math.floor(1000 + Math.random() * 9000));
24 |
25 | useEffect(() => {
26 | setCopyText("Copy to clipboard");
27 |
28 | setTextboxContent(displayedCommand ? displayedCommand.replace("$fileName", `winstall-${downloadId}.json`) : fileContent);
29 | }, [fileContent, displayedCommand, downloadId])
30 |
31 | return (
32 |
70 | )
71 | }
72 |
73 | export default GenericExport;
--------------------------------------------------------------------------------
/components/AppExport/InputComponents.js:
--------------------------------------------------------------------------------
1 | import styles from "../../styles/exportApps.module.scss";
2 |
3 | export const CheckboxConfig = ({ id, defaultChecked, updateConfig, hiddenOptions, labelText }) => {
4 | if(hiddenOptions.includes(id)) return null;
5 |
6 | return (
7 |
8 | updateConfig(id, e.target.checked)}/>
9 | {labelText} {id}
10 |
11 | )
12 | }
13 |
14 | export const RadioConfig = ({ id, defaultChecked, options, updateConfig, hiddenOptions, labelText }) => {
15 | if(hiddenOptions.includes(id)) return null;
16 |
17 | return (
18 |
19 | {labelText} {id}
20 |
21 |
22 | { options.map((option) => {
23 | return (
24 |
25 | updateConfig(id, e.target.value)} checked={defaultChecked === option.id}/>
26 | {option.label}
27 |
28 | )
29 | })}
30 |
31 |
32 | )
33 | }
34 |
35 | export const TextInputConfig = ({ id, defaultValue, updateConfig, hiddenOptions, labelText, inputPlaceholder }) => {
36 | if(hiddenOptions.includes(id)) return null;
37 |
38 | return (
39 |
40 | {labelText} {id}
41 | updateConfig(id, e.target.value)} placeholder={inputPlaceholder}/>
42 |
43 | )
44 | }
--------------------------------------------------------------------------------
/components/AppIcon.js:
--------------------------------------------------------------------------------
1 | import styles from "../styles/singleApp.module.scss";
2 | import LazyLoad from "react-lazyload";
3 | import popularAppsList from "../data/popularApps.json";
4 |
5 | const AppIcon = ({id, name, icon}) => {
6 | // if the app is listed in popularApps, use the image specified there
7 | const popularApps = Object.values(popularAppsList).filter((app) => app._id === id);
8 | if (popularApps.length === 1) {
9 | return (
10 |
15 | );
16 | }
17 |
18 | if(!icon) {
19 | return (
20 |
27 | );
28 | }
29 |
30 | if (icon.startsWith("http")) {
31 | return (
32 |
33 | { // if icon is not hosted on winstall
34 | icon.startsWith("http") && (
35 |
43 | )
44 | }
45 |
46 | );
47 | }
48 |
49 | icon = icon.replace(".png", "")
50 |
51 | return (
52 |
57 | );
58 | }
59 |
60 | const AppPicture = ({ name, srcSetPng, srcSetWebp }) => {
61 | return (
62 |
63 |
64 |
65 |
66 |
74 |
75 |
76 | );
77 | }
78 |
79 | export default AppIcon;
--------------------------------------------------------------------------------
/components/CreatePackForm.js:
--------------------------------------------------------------------------------
1 | import { useForm } from "react-hook-form";
2 | import { useState } from "react";
3 | import { useRouter } from "next/router";
4 | import styles from "../styles/create.module.scss";
5 | import Link from "next/link";
6 | import fetchWinstallAPI from "../utils/fetchWinstallAPI";
7 |
8 | const CreatePackForm = ({ user, packApps, editMode, defaultValues, isDisabled }) => {
9 | const { handleSubmit, register, errors } = useForm({ defaultValues });
10 | const [creating, setCreating] = useState(false);
11 | const [created, setCreated] = useState();
12 | const [error, setError] = useState("");
13 | const router = useRouter();
14 |
15 | const onSubmit = async (values) => {
16 | setCreating(true);
17 |
18 | const apps = packApps.map(app => {
19 | return {
20 | _id: app._id,
21 | name: app.name,
22 | icon: app.icon
23 | }
24 | })
25 |
26 | if(apps.length < 5){
27 | setError(`You need at least 5 apps to be able to ${editMode ? "update" : "create"} this pack.`);
28 | setCreating(false);
29 | return;
30 | }
31 |
32 | const requestPath = editMode ? `/packs/${defaultValues._id}` : `/packs/create`;
33 | const requestOptions = {
34 | method: editMode ? 'PATCH' : 'POST',
35 | headers: {
36 | 'Authorization': `${user.accessToken},${user.refreshToken}`,
37 | 'Content-Type': 'application/json'
38 | },
39 | body: JSON.stringify({
40 | title: values.title,
41 | desc: values.description,
42 | apps: JSON.stringify(apps),
43 | creator: user.id,
44 | accent: values.accent,
45 | isUnlisted: values.isUnlisted
46 | }),
47 | redirect: 'follow'
48 | }
49 |
50 | const { response, error } = await fetchWinstallAPI(requestPath, requestOptions);
51 |
52 | if(error){
53 | setCreating(false);
54 | setError(error);
55 | return;
56 | }
57 |
58 | if(response){
59 | localStorage.removeItem("ownPacks");
60 | router.push(`/packs/${response._id}`);
61 | setCreated(response);
62 | }
63 | };
64 |
65 | if(created){
66 | return (
67 | Your pack has been sucesfully {editMode ? "updated" : "created"}! You can view it here: winstall.app/packs/{created._id}
68 | )
69 | }
70 |
71 | return (
72 |
131 | );
132 | }
133 |
134 | export default CreatePackForm;
--------------------------------------------------------------------------------
/components/DonateCard.js:
--------------------------------------------------------------------------------
1 | import styles from "../styles/donateCard.module.scss";
2 | import { FiPlus } from "react-icons/fi";
3 | import { RiPaypalFill } from "react-icons/ri";
4 |
5 | const DonateCard = ({ addMargin = "both" }) => {
6 | return (
7 |
8 |
Seamless Intune Integration, Effortless App Management with Pckgr
9 |
Pckgr simplifies your Intune application management. Utilizing winget, it streamlines software deployment with easy, one-click processes and automatic updates. Experience a user-friendly, secure way to manage apps across your enterprise. Start your 30 day free trial today!
10 |
13 |
14 | )
15 | }
16 |
17 | export default DonateCard;
--------------------------------------------------------------------------------
/components/Error.js:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 |
3 | import styles from "../styles/error.module.scss";
4 |
5 | import {FiHome} from "react-icons/fi";
6 |
7 | function Error({title, subtitle}) {
8 | return (
9 |
10 |
15 |
{title ? title : "Something went terribly wrong."}
16 |
17 | {subtitle
18 | ? subtitle
19 | : "It's looks like you're trying to find something that doesn't exist, would you like some help with that?"}
20 |
21 |
22 |
23 |
24 |
25 | Go Home
26 |
27 |
28 |
29 |
30 | );
31 | }
32 |
33 | export default Error;
34 |
--------------------------------------------------------------------------------
/components/FeaturePromoter.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { FiX } from "react-icons/fi";
3 | import styles from "../styles/featurePromoter.module.scss";
4 |
5 | const FeaturePromoter = ({children, art, promoId, disableHide=false}) => {
6 | const [hidden, setHidden] = useState(true);
7 |
8 | useEffect(() => {
9 | if(disableHide) {
10 | setHidden(false);
11 | return;
12 | }
13 |
14 | if(!promoId) return;
15 |
16 | const isHidden = localStorage.getItem(`featurePromo-${promoId}`);
17 |
18 | if(isHidden === null) setHidden(false);
19 | }, []);
20 |
21 | const handleHide = () => {
22 | setHidden(true);
23 |
24 | if(!promoId) return;
25 |
26 | localStorage.setItem(`featurePromo-${promoId}`, true);
27 | }
28 |
29 | if(hidden) return null;
30 |
31 | return (
32 |
33 | {!disableHide &&
}
34 | {art &&
}
35 |
36 | {children}
37 |
38 |
39 | )
40 | }
41 |
42 | export default FeaturePromoter;
--------------------------------------------------------------------------------
/components/Footer.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useContext } from "react";
2 | import styles from "../styles/footer.module.scss";
3 | import Link from "next/link";
4 | import { FiChevronUp } from "react-icons/fi";
5 | import SelectedContext from "../ctx/SelectedContext";
6 |
7 | export default function Footer() {
8 | const { selectedApps, setSelectedApps } = useContext(SelectedContext);
9 |
10 | useEffect(() => {
11 | window.onscroll = function () {
12 | scrollFunction();
13 | };
14 | }, []);
15 |
16 | const scrollFunction = () => {
17 | const btnTop = document.getElementById("btnTop");
18 |
19 | if (
20 | (document.body.scrollTop > 20 ||
21 | document.documentElement.scrollTop > 20) &&
22 | btnTop
23 | ) {
24 | btnTop.style.display = "flex";
25 | } else if (btnTop) {
26 | btnTop.style.display = "none";
27 | }
28 | };
29 |
30 | const topFunction = () => {
31 | document.body.scrollTop = 0;
32 | document.documentElement.scrollTop = 0;
33 | };
34 |
35 | return (
36 |
92 | );
93 | }
94 |
--------------------------------------------------------------------------------
/components/ListPackages.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styles from "../styles/appList.module.scss";
3 |
4 | function ListPackages({children, popular}) {
5 | return ;
6 | }
7 |
8 | export default ListPackages;
--------------------------------------------------------------------------------
/components/ListSort.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import styles from "../styles/listSort.module.scss";
3 | import { FiChevronDown } from "react-icons/fi";
4 |
5 | const ListSort = ({apps, defaultSort, onSort}) => {
6 | const [sort, setSort] = useState("");
7 |
8 | useEffect(() => {
9 | if(!sort){
10 | setSort(defaultSort);
11 | }
12 | })
13 |
14 | let onSortSelected = (e) => {
15 | let sortChoice = e.target.value;
16 | setSort(sortChoice);
17 | applySort(apps, sortChoice);
18 | onSort(sortChoice);
19 | };
20 |
21 | return (
22 |
23 | Sort by
24 | onSortSelected(e)}>
25 | Recently Updated (Newest First)
26 | Recently Updated (Oldest First)
27 | Name (Ascending)
28 | Name (Descending)
29 |
30 |
31 |
32 | );
33 | }
34 |
35 | function applySort(apps, sortChoice) {
36 | if (sortChoice === "name-asc") {
37 | apps.sort((a, b) => a.name.localeCompare(b.name));
38 | } else if (sortChoice === "name-desc") {
39 | apps.sort((a, b) => b.name.localeCompare(a.name));
40 | } else if (sortChoice === "update-desc") {
41 | // because the updatedAt values are in ISO, we can just to lexographical comparison
42 | apps.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
43 | } else if (sortChoice === "update-asc") {
44 | apps.sort((a, b) => a.updatedAt.localeCompare(b.updatedAt));
45 | } else {
46 | throw new Error("Invalid sortChoice: " + sortChoice);
47 | }
48 | };
49 |
50 | module.exports = {
51 | ListSort,
52 | applySort
53 | };
--------------------------------------------------------------------------------
/components/MetaTags.js:
--------------------------------------------------------------------------------
1 | import Head from "next/head";
2 |
3 | const MetaTags = ({ title, desc="Bulk install Windows apps quickly with Windows Package Manager." }) => {
4 | return (
5 |
6 | {title}
7 |
11 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
27 |
28 |
29 |
30 |
31 |
32 |
36 |
40 |
41 |
42 |
43 |
44 | );
45 | }
46 |
47 | export default MetaTags;
--------------------------------------------------------------------------------
/components/Nav.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState, useRef } from "react";
2 | import { useRouter } from "next/router";
3 | import Router from "next/router";
4 |
5 | import { useSession, signIn, signOut, getSession } from "next-auth/react";
6 |
7 | import Link from "next/link";
8 | import styles from "../styles/nav.module.scss";
9 | import {
10 | FiMoon,
11 | FiSun,
12 | FiPackage,
13 | FiTwitter,
14 | FiLogOut,
15 | FiGrid,
16 | FiChevronDown,
17 | FiX,
18 | } from "react-icons/fi";
19 |
20 | import NProgress from "nprogress";
21 |
22 | Router.onRouteChangeStart = () => {
23 | NProgress.start();
24 | };
25 |
26 | Router.onRouteChangeComplete = () => {
27 | NProgress.done();
28 | };
29 |
30 | Router.onRouteChangeError = () => {
31 | NProgress.done();
32 | };
33 |
34 | function Nav() {
35 | const [ddShown, setDDShown] = useState(false);
36 | const navRef = useRef(null);
37 |
38 | let handleClickOut = (e) => {
39 | if (navRef.current && !navRef.current.contains(e.target)) {
40 | setDDShown(false);
41 | navRef.current.classList.remove("shown");
42 | }
43 |
44 | if (navRef.current && navRef.current.contains(e.target)) {
45 | setDDShown(false);
46 | setTimeout(() => {
47 | navRef.current.classList.remove("shown");
48 | }, 200);
49 | }
50 | };
51 |
52 | useEffect(() => {
53 | window.addEventListener("mousedown", handleClickOut);
54 |
55 | // cleanup this component
56 | return () => {
57 | window.removeEventListener("mousedown", handleClickOut);
58 | };
59 | }, []);
60 |
61 | let switchTheme = () => {
62 | let body = document.querySelector("body");
63 |
64 | if (body.classList.contains("light")) {
65 | localStorage.setItem("wiTheme", "dark");
66 | body.classList.replace("light", "dark");
67 | } else {
68 | localStorage.setItem("wiTheme", "light");
69 | body.classList.replace("dark", "light");
70 | }
71 | };
72 |
73 | const toggleDD = () => {
74 | if (ddShown) {
75 | navRef.current.classList.remove("shown");
76 | } else {
77 | navRef.current.classList.add("shown");
78 | }
79 |
80 | setDDShown(!ddShown);
81 | };
82 |
83 | return (
84 |
85 |
86 |
87 |
winstall
88 |
89 | {/*
(preview) */}
90 |
91 |
92 |
115 |
116 |
117 | {ddShown ? : }
118 |
119 |
120 | );
121 | }
122 |
123 | const User = () => {
124 | const { data: session } = useSession();
125 |
126 | const [user, setUser] = useState();
127 |
128 | useEffect(() => {
129 | getSession().then(async (session) => {
130 | if (!session) return;
131 | const { response, error } = await fetch("/api/twitter/", {
132 | method: "GET",
133 | headers: {
134 | endpoint: `https://api.twitter.com/2/users/${session.user.id}`,
135 | },
136 | }).then((res) => res.json());
137 |
138 | if (!error) {
139 | setUser(response);
140 | }
141 | });
142 | }, []);
143 |
144 | return null;
145 | };
146 |
147 | export default Nav;
148 |
--------------------------------------------------------------------------------
/components/PackAppsList.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useCallback } from "react";
2 | import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd";
3 |
4 | import styles from "../styles/packsAppList.module.scss";
5 | import SingleApp from "./SingleApp";
6 | import DonateCard from "./DonateCard";
7 |
8 | import { FiPlus, FiPlusCircle, FiXCircle } from "react-icons/fi";
9 | import Search from "./Search";
10 | import Modal from "react-modal";
11 |
12 | Modal.setAppElement("#__next");
13 |
14 | const AppsList = ({ apps, onListUpdate }) => {
15 |
16 | return apps.map((app, index) => (
17 |
18 | ));
19 | };
20 |
21 | const reorder = (list, startIndex, endIndex) => {
22 | const result = Array.from(list);
23 | const [removed] = result.splice(startIndex, 1);
24 | result.splice(endIndex, 0, removed);
25 |
26 | return result;
27 | };
28 |
29 | function App({ app, index, onListUpdate }) {
30 | return (
31 |
32 | {(provided) => (
33 |
39 |
40 | onListUpdate(app._id)}
43 | aria-label={"Remove app from pack"}
44 | >
45 |
46 |
47 |
48 | )}
49 |
50 | );
51 | }
52 |
53 | function PackAppsList({ notLoggedIn = false, providedApps, reorderEnabled, onListUpdate, allApps}){
54 |
55 | const [apps, setApps] = useState([]);
56 | const [, updateState] = useState();
57 | const forceUpdate = useCallback(() => updateState({}), []);
58 | const [showAdd, setShowAdd] = useState(false);
59 |
60 | useEffect(() => {
61 | setApps(providedApps)
62 | }, [])
63 |
64 | if(!reorderEnabled){
65 | return (
66 |
67 | {apps.map((app, index) => (
68 |
69 |
70 |
71 |
72 |
73 | { index === 3 && }
74 |
75 | ))}
76 |
77 | );
78 | }
79 |
80 | const onDragEnd = (result) => {
81 | if (!result.destination) {
82 | return;
83 | }
84 |
85 | if (result.destination.index === result.source.index) {
86 | return;
87 | }
88 |
89 | const updatedApps = reorder(
90 | apps,
91 | result.source.index,
92 | result.destination.index
93 | );
94 |
95 |
96 | onListUpdate(updatedApps)
97 |
98 | setApps(updatedApps);
99 | }
100 |
101 | const manageUpdates = (id) => {
102 | const updatedApps = apps.filter(i => i._id !== id);
103 | setApps(updatedApps);
104 | onListUpdate(updatedApps);
105 | }
106 |
107 | const handleSelect = (app, isSelected) => {
108 | let existingApps = apps;
109 |
110 | if (isSelected) {
111 | existingApps.push(app);
112 | } else {
113 | existingApps = existingApps.filter(a => a._id !== app._id);
114 | }
115 |
116 | setApps(existingApps);
117 | onListUpdate(existingApps);
118 |
119 | forceUpdate();
120 | }
121 |
122 | const closeModal = () => {
123 | setShowAdd(false);
124 | }
125 |
126 | return (
127 | <>
128 | Apps in this pack
129 | {!notLoggedIn && Tip! Drag and drop any app to re-order how they appear in your pack.
}
130 |
131 | setShowAdd(!showAdd)}> {`Add ${apps.length != 0 ? "More" : ""} Apps`}
132 |
133 |
140 |
141 |
Add Apps
142 |
143 |
144 |
145 |
146 |
147 | {notLoggedIn ? You need to login first before you can view the apps in this pack.
: (
148 |
149 |
150 | {(provided) => (
151 |
156 |
157 | {provided.placeholder}
158 |
159 | )}
160 |
161 |
162 | ) }
163 | >
164 | );
165 |
166 | }
167 |
168 | export default PackAppsList;
--------------------------------------------------------------------------------
/components/PackPreview.js:
--------------------------------------------------------------------------------
1 | import styles from "../styles/packPreview.module.scss";
2 | import AppIcon from "./AppIcon";
3 | import { useState, useEffect} from "react";
4 | import { FiEdit, FiPackage, FiTrash2 } from "react-icons/fi";
5 | import Link from "next/link";
6 | import fetchWinstallAPI from "../utils/fetchWinstallAPI";
7 |
8 | export default function PackPreview({ pack, hideMeta, showDelete=false, auth, deleted}){
9 | const [appIcons, setIcons] = useState([]);
10 |
11 | useEffect(() => {
12 | const appIcons = pack.apps.filter(a => a.icon != "" ).map(a => ({ icon: a.icon, _id: a._id }));
13 |
14 | setIcons([...appIcons].slice(0, 4));
15 |
16 | }, [])
17 |
18 | const deletePack = async () => {
19 | if(!auth) return;
20 |
21 | const { response } = await fetchWinstallAPI(`/packs/${pack._id}`, {
22 | method: "DELETE",
23 | headers: {
24 | 'Authorization': `${auth.accessToken},${auth.refreshToken}`,
25 | 'Content-Type': 'application/json'
26 | },
27 | body: JSON.stringify({ creator: pack.creator })
28 | });
29 |
30 | if(response && response.msg){
31 | if(deleted) deleted(pack._id);
32 | localStorage.removeItem("ownPacks");
33 | }
34 | }
35 |
36 | const handleDelete = async (e) => {
37 | if ('confirm' in window && typeof window.confirm === 'function') {
38 | if (window.confirm("Are you sure you want to delete this pack?")) {
39 | deletePack();
40 | }
41 | } else {
42 | deletePack();
43 | }
44 |
45 | }
46 |
47 | return (
48 |
83 | )
84 | }
--------------------------------------------------------------------------------
/components/PageWrapper.js:
--------------------------------------------------------------------------------
1 | import Footer from "./Footer";
2 |
3 | function PageWrapper({ children }){
4 | return (
5 |
6 | {children}
7 |
8 |
9 |
10 | )
11 | }
12 |
13 | export default PageWrapper;
--------------------------------------------------------------------------------
/components/PopularApps.js:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import PrettyApp from "../components/PrettyApp";
3 | import ListPackages from "../components/ListPackages";
4 |
5 | import PopularContext from "../ctx/PopularContext";
6 |
7 | import { FiPackage } from "react-icons/fi";
8 | import { useEffect, useContext } from "react";
9 |
10 | let PopularApps = ({ apps, all }) => {
11 | const { popular, setPopular } = useContext(PopularContext);
12 |
13 | useEffect(() => {
14 | if(popular.length === 0){
15 | setPopular(apps.slice(0, 16));
16 | }
17 | })
18 |
19 | return (
20 |
21 | {({ popular }) => (
22 |
23 |
32 |
33 | The essentials for your new Windows device.
34 |
35 |
36 | {popular.map((app) => (
37 |
38 | ))}
39 |
40 |
41 | )}
42 |
43 | );
44 | };
45 |
46 | export default PopularApps;
--------------------------------------------------------------------------------
/components/PrettyApp.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useContext, useEffect } from "react";
2 | import styles from "../styles/prettyApp.module.scss";
3 | import SelectedContext from "../ctx/SelectedContext";
4 | import Link from "next/link";
5 | import { FiPlus } from "react-icons/fi";
6 |
7 | let PrettyApp = ({ app, all }) => {
8 | const [selected, setSelected] = useState(false);
9 | const { selectedApps, setSelectedApps } = useContext(SelectedContext);
10 |
11 | useEffect(() => {
12 | let found = selectedApps.findIndex((a) => a._id === app._id) !== -1;
13 |
14 | setSelected(found);
15 | }, [selectedApps, app._id]);
16 |
17 | let handleAppSelect = () => {
18 | let found = selectedApps.findIndex((a) => a._id === app._id);
19 | if (found !== -1) {
20 | let updatedSelectedApps = selectedApps.filter(
21 | (a, index) => index !== found
22 | );
23 |
24 | setSelectedApps(updatedSelectedApps);
25 | setSelected(false);
26 | } else {
27 | setSelected(true);
28 |
29 | if (all) {
30 | app = all.find((i) => app._id == i._id);
31 |
32 | setSelectedApps([...selectedApps, app]);
33 | } else {
34 | setSelectedApps([...selectedApps, app]);
35 | }
36 | }
37 | };
38 |
39 | if (!app && !app.img) return <>>;
40 |
41 | return (
42 |
43 |
47 |
48 |
49 |
50 |
52 |
59 |
67 |
68 |
69 |
{app.name}
70 |
71 |
72 |
73 | );
74 | };
75 |
76 | export default PrettyApp;
77 |
--------------------------------------------------------------------------------
/components/RecentApps.js:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import SingleApp from "../components/SingleApp";
3 | import ListPackages from "../components/ListPackages";
4 |
5 | import { FiPackage } from "react-icons/fi";
6 |
7 | let RecentApps = ({ apps }) => {
8 |
9 | return (
10 |
11 |
20 |
21 | All the newest apps and updates. Click the + sign to include an app on your install script.
22 |
23 |
24 | {apps.map((app) => (
25 |
26 | ))}
27 |
28 |
29 | );
30 | };
31 |
32 | export default RecentApps;
33 |
--------------------------------------------------------------------------------
/components/Recommendations.js:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 |
3 | import styles from "../styles/recommendations.module.scss"
4 | import { FiPackage, FiPlus, FiGlobe, FiHome, FiChevronRight, FiCrosshair, FiUserPlus, FiZap, FiMusic, FiCode, FiStar, FiBookOpen } from "react-icons/fi";
5 | import { useEffect, useContext, useState, useRef } from "react";
6 | import PackAppsList from "./PackAppsList";
7 | import AppIcon from "./AppIcon";
8 | import SelectedContext from "../ctx/SelectedContext";
9 | import PackPreview from "./PackPreview";
10 |
11 | const Recommendations = ({ packs }) => {
12 | return (
13 |
14 |
23 |
24 | Just got a new Windows device? Start with our favourites. Click the +
25 | sign to include an app on your install script.
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | );
63 | };
64 |
65 | const PackList = ({ children, title, id, packs}) => {
66 | const [packApps, setPackApps] = useState([]);
67 | const [pack, setPack] = useState();
68 | const headerRef = useRef(null);
69 |
70 | useEffect(() => {
71 | if (!packs) return;
72 |
73 | const pack = packs.find(p => p._id === id);
74 |
75 | if(!pack) return;
76 |
77 | setPackApps(pack.apps.slice(0, 5));
78 | setPack(pack);
79 |
80 | }, []);
81 |
82 | if(!pack) return <>>;
83 |
84 | return (
85 |
104 | );
105 | };
106 |
107 | const App = ({ data }) => {
108 | const [selected, setSelected] = useState(false);
109 | const { selectedApps, setSelectedApps } = useContext(SelectedContext);
110 |
111 | useEffect(() => {
112 | let found = selectedApps.find((a) => a._id === data._id);
113 |
114 | setSelected(found);
115 | }, [ data, selectedApps ])
116 |
117 | let handleAppSelect = () => {
118 | let found = selectedApps.findIndex((a) => a._id === data._id);
119 |
120 | if (found !== -1) {
121 | let updatedSelectedApps = selectedApps.filter(
122 | (a, index) => index !== found
123 | );
124 |
125 | setSelectedApps(updatedSelectedApps);
126 | setSelected(false);
127 |
128 | } else if(data) {
129 | setSelected(true);
130 |
131 | setSelectedApps([...selectedApps, data]);
132 | }
133 |
134 | };
135 |
136 | return (
137 |
146 | )
147 | }
148 |
149 |
150 | export default Recommendations;
151 |
--------------------------------------------------------------------------------
/components/Search.js:
--------------------------------------------------------------------------------
1 | import{ useState, useEffect } from "react";
2 | import styles from "../styles/search.module.scss";
3 |
4 | import { DebounceInput } from "react-debounce-input";
5 | import Fuse from "fuse.js";
6 |
7 | import SingleApp from "../components/SingleApp";
8 | import ListPackages from "../components/ListPackages";
9 |
10 | import { FiSearch, FiHelpCircle } from "react-icons/fi";
11 | import { forceVisible } from 'react-lazyload';
12 | import { useRouter } from "next/router";
13 |
14 | function Search({ apps, onSearch, label, placeholder, preventGlobalSelect, isPackView, alreadySelected=[], limit=-1}) {
15 | const [results, setResults] = useState([])
16 | const [searchInput, setSearchInput] = useState();
17 | const defaultKeys = [{ name: "moniker", weight: 2 }, { name: "name", weight: 2 }, "path", "desc", "publisher", "tags"];
18 | const [keys, setKeys] = useState(defaultKeys);
19 | const router = useRouter();
20 | const [urlQuery, setUrlQuery] = useState();
21 |
22 | const options = (keys) => {
23 | return {
24 | minMatchCharLength: 3,
25 | threshold: 0.3,
26 | keys
27 | }
28 | }
29 |
30 | let fuse = new Fuse(apps, options(defaultKeys));
31 |
32 | useEffect(() => {
33 | // if we have a ?q param on the url, we deal with it
34 | if (apps.length !== 0 && router.query && router.query.q && urlQuery !== router.query.q){
35 | handleSearchInput(null, router.query.q)
36 | setUrlQuery(router.query.q)
37 | } else if(results != 0 && urlQuery && router.query && !router.query.q){
38 | // if we previously had a query, going back should reset it.
39 | setSearchInput("");
40 | setResults([]);
41 | onSearch();
42 | }
43 | })
44 |
45 | const handleSearchInput = (e, q) => {
46 | const inputVal = e ? e.target.value : q;
47 |
48 | if(onSearch) onSearch(inputVal);
49 |
50 | let query = inputVal;
51 |
52 | if(query === ""){
53 | forceVisible(); // for some reason lazy load doesn't detect when the new elements roll in, so we force visible to all imgs
54 | }
55 |
56 | let prefixes = ["name", "tags", "publisher", "desc"];
57 | let checkPrefix = prefixes.filter(prefix => query.startsWith(`${prefix}:`));
58 |
59 |
60 | if(checkPrefix.length !== 0){
61 | setKeys(checkPrefix);
62 | query = query.replace(`${checkPrefix[0]}:`, "")
63 | fuse = new Fuse(apps, options(checkPrefix));
64 | } else if(keys !== defaultKeys){
65 | setKeys(defaultKeys)
66 | fuse = new Fuse(apps, options(defaultKeys));
67 | }
68 |
69 | setSearchInput(inputVal);
70 |
71 | if (query<= 3) return;
72 |
73 | let results = fuse.search(query.toLowerCase().replace(/\s/g, ""));
74 |
75 | setResults([...results.map((r) => r.item).slice(0, (limit ? limit : results.length))]);
76 | };
77 |
78 |
79 | if (!apps) return <>>;
80 |
81 | return (
82 |
83 |
{label || `${Math.floor(apps.length / 50) * 50}+ packages and growing.`}
84 |
85 |
86 |
87 |
88 | handleSearchInput(e)}
92 | id="search"
93 | value={searchInput}
94 | autoComplete="off"
95 | autoFocus={true}
96 | placeholder={placeholder || "Search for apps here"}
97 | />
98 |
99 |
100 |
101 |
102 |
103 |
Use search prefixes to target a specific field in searches!
104 |
105 | name:
search for an app's name
106 | publisher:
search for apps by a publisher
107 | tags:
search for apps by a tag
108 | desc:
search the description of apps
109 |
110 |
111 |
112 | {searchInput && results.length === limit &&
113 |
114 | Showing {results.length} result
115 | {results.length > 1 && "s"}
116 | . {results.length == limit &&
117 | More
118 | }
119 |
120 | }
121 |
122 |
123 | {searchInput && results.length !== 0 ? (
124 |
125 | {results.map((app, i) =>
126 | a._id === app._id) != -1 ? true : false}
134 | />
135 | )}
136 |
137 | ) : (
138 | <>{searchInput ?
Could not find any apps.
: ""}>
139 | )}
140 |
141 | );
142 | }
143 |
144 | export default Search;
145 |
--------------------------------------------------------------------------------
/components/SelectionBar.js:
--------------------------------------------------------------------------------
1 | import React, { useContext, useEffect, useState } from "react";
2 | import Link from 'next/link'
3 | import SelectedContext from "../ctx/SelectedContext";
4 | import { withRouter } from 'next/router'
5 |
6 | import { FiTrash, FiCodepen, FiShare } from "react-icons/fi";
7 |
8 | function SelectionBar({ router }) {
9 | const { selectedApps, setSelectedApps } = useContext(SelectedContext);
10 | const [ hideCreatePack, setHideCreatePack ] = useState(false);
11 |
12 | useEffect(() => {
13 | if(router.pathname === "/packs/create"){
14 | setHideCreatePack(true);
15 | } else{
16 | setHideCreatePack(false);
17 | };
18 | }, [ router ])
19 |
20 | if(selectedApps.length === 0) return <>>;
21 |
22 | let handleClear = () => {
23 | // check if confirm exists
24 | // support for iOS safari
25 | if ('confirm' in window && typeof window.confirm === 'function'){
26 | if(window.confirm("Are you sure you want to unselect all the apps?")){
27 | setSelectedApps([]);
28 | }
29 | } else{
30 | setSelectedApps([]);
31 | }
32 | }
33 | return (
34 |
35 |
36 |
37 |
Selected {selectedApps.length} apps so far
38 |
39 |
40 | handleClear()} title="Clear selections">
41 |
42 |
43 | { !hideCreatePack && (
44 |
45 | = 5 ? false : true}>
46 |
47 | {selectedApps.length >= 5 ? "Create Pack" : `Need ${Math.abs(5 - selectedApps.length)} more ${Math.abs(5 - selectedApps.length) === 1 ? "app" : "apps"} to create a pack.`}
48 |
49 |
50 | )}
51 |
52 |
53 |
54 | Generate script
55 |
56 |
57 |
58 |
59 |
60 | );
61 | }
62 |
63 | export default withRouter(SelectionBar);
--------------------------------------------------------------------------------
/ctx/PopularContext.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 |
4 | const PopularContext = React.createContext({
5 | popular: [],
6 | setPopular: () => {},
7 | });
8 |
9 |
10 | export default PopularContext;
--------------------------------------------------------------------------------
/ctx/SelectedContext.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const SelectedContext = React.createContext({
4 | selected: [],
5 | setSelected: () => {},
6 | });
7 |
8 | export default SelectedContext;
9 |
--------------------------------------------------------------------------------
/data/popularApps.json:
--------------------------------------------------------------------------------
1 | {
2 | "spotify": {
3 | "img": "spotify.webp",
4 | "name": "Spotify",
5 | "_id": "Spotify.Spotify",
6 | "path": "Spotify/Spotify"
7 | },
8 | "discord": {
9 | "img": "discord.webp",
10 | "name": "Discord",
11 | "_id": "Discord.Discord",
12 | "path": "Discord/Discord"
13 | },
14 | "vscode": {
15 | "img": "code.webp",
16 | "name": "Visual Studio Code",
17 | "_id": "Microsoft.VisualStudioCode",
18 | "path": "Microsoft/VisualStudioCode"
19 | },
20 | "vscode-insiders": {
21 | "img": "code-insiders.webp",
22 | "name": "Visual Studio Code - Insiders",
23 | "_id": "Microsoft.VisualStudioCode.Insiders",
24 | "path": "Microsoft/VisualStudioCodeInsiders"
25 | },
26 | "chrome": {
27 | "img": "chrome.webp",
28 | "name": "Google Chrome",
29 | "_id": "Google.Chrome",
30 | "path": "Google/Chrome"
31 | },
32 | "firefox": {
33 | "img": "firefox.webp",
34 | "name": "Mozilla Firefox",
35 | "_id": "Mozilla.Firefox",
36 | "path": "Mozilla/Firefox"
37 | },
38 | "notion": {
39 | "img": "notion.webp",
40 | "name": "Notion",
41 | "_id": "Notion.Notion",
42 | "path": "Notion/Notion"
43 | },
44 | "python": {
45 | "img": "python.webp",
46 | "name": "Python",
47 | "_id": "Python.Python",
48 | "path": "Python/Python"
49 | },
50 | "whatsapp": {
51 | "img": "whatsapp.webp",
52 | "name": "WhatsApp",
53 | "_id": "WhatsApp.WhatsApp",
54 | "path": "WhatsApp/WhatsApp"
55 | },
56 | "zoom": {
57 | "img": "zoom.webp",
58 | "name": "Zoom",
59 | "_id": "Zoom.Zoom",
60 | "path": "Zoom/Zoom"
61 | },
62 | "dropbox": {
63 | "img": "dropbox.webp",
64 | "name": "Dropbox",
65 | "_id": "Dropbox.Dropbox",
66 | "path": "Dropbox/Dropbox"
67 | },
68 | "npp": {
69 | "img": "npp.webp",
70 | "name": "Notepad++",
71 | "_id": "Notepad++.Notepad++",
72 | "path": "Notepad++/Notepad++"
73 | },
74 | "node": {
75 | "img": "node.webp",
76 | "name": "Node.js",
77 | "_id": "OpenJS.NodeJS",
78 | "path": "OpenJS/NodeJS"
79 | },
80 | "obsstudio": {
81 | "img": "obs.webp",
82 | "name": "OBS Studio",
83 | "_id": "OBSProject.OBSStudio",
84 | "path": "OBSProject/OBSStudio"
85 | },
86 | "edge": {
87 | "img": "edge.webp",
88 | "name": "Microsoft Edge",
89 | "_id": "Microsoft.Edge",
90 | "path": "Microsoft/Edge"
91 | },
92 | "teams": {
93 | "img": "teams.webp",
94 | "name": "Microsoft Teams",
95 | "_id": "Microsoft.Teams",
96 | "path": "Microsoft/Teams"
97 | },
98 | "windowsterminal": {
99 | "img": "terminal.webp",
100 | "name": "Windows Terminal",
101 | "_id": "Microsoft.WindowsTerminal",
102 | "path": "Microsoft/WindowsTerminal"
103 | },
104 | "eartrumpet": {
105 | "img": "trumpet.webp",
106 | "name": "EarTrumpet",
107 | "_id": "File-New-Project.EarTrumpet",
108 | "path": "File-New-Project/EarTrumpet"
109 | },
110 | "tweeten": {
111 | "img": "tweeten.webp",
112 | "name": "Tweeten",
113 | "_id": "MehediHassan.Tweeten",
114 | "path": "MehediHassan/Tweeten"
115 | },
116 | "sharex": {
117 | "img": "sharex.webp",
118 | "name": "ShareX",
119 | "_id": "ShareX.ShareX",
120 | "path": "ShareX/ShareX"
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | const withPWA = require("next-pwa")({
2 | dest: "public",
3 | disable: process.env.NODE_ENV === "development",
4 | });
5 |
6 | module.exports = withPWA({
7 | // next.js config
8 | });
9 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "winstall",
3 | "version": "2.0.0",
4 | "private": true,
5 | "dependencies": {
6 | "fuse.js": "^6.0.3",
7 | "next": "^12.0.8",
8 | "next-auth": "^4.23.1",
9 | "next-pwa": "^5.6.0",
10 | "nprogress": "^0.2.0",
11 | "react": "^17.0.2",
12 | "react-debounce-input": "^3.2.2",
13 | "react-dom": "16.13.1",
14 | "react-hook-form": "^5.7.2",
15 | "react-icons": "^3.10.0",
16 | "react-lazyload": "^3.2.0",
17 | "react-loading-skeleton": "^2.0.1",
18 | "react-modal": "^3.11.2",
19 | "react-scripts": "3.4.1",
20 | "react-toggle": "^4.1.1",
21 | "yaml": "^1.10.0"
22 | },
23 | "scripts": {
24 | "dev": "next",
25 | "build": "next build",
26 | "start": "next start",
27 | "export": "next export"
28 | },
29 | "devDependencies": {
30 | "@types/react": "^17.0.38",
31 | "react-beautiful-dnd": "^13.0.0",
32 | "sass": "^1.49.0"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/pages/404.js:
--------------------------------------------------------------------------------
1 | import Error from "../components/Error";
2 | import MetaTags from "../components/MetaTags";
3 |
4 | export default function Custom404() {
5 | return (
6 | <>
7 |
8 |
9 | >
10 | )
11 | }
12 |
--------------------------------------------------------------------------------
/pages/_app.js:
--------------------------------------------------------------------------------
1 | import "../styles/base.scss";
2 | import { useState, useEffect } from "react";
3 |
4 | import SelectedContext from "../ctx/SelectedContext";
5 |
6 | import { checkTheme } from "../utils/helpers";
7 | import Nav from "../components/Nav";
8 | import SelectionBar from "../components/SelectionBar";
9 | import PopularContext from "../ctx/PopularContext";
10 | import { SessionProvider } from "next-auth/react";
11 |
12 | function winstall({ Component, pageProps: { session, ...pageProps } }) {
13 | const [selectedApps, setSelectedApps] = useState([]);
14 | const selectedAppValue = { selectedApps, setSelectedApps };
15 |
16 | const [popular, setPopular] = useState([]);
17 | const popularApps = { popular, setPopular };
18 |
19 | useEffect(() => {
20 | checkTheme();
21 | }, []);
22 |
23 | return (
24 |
25 |
26 |
27 | <>
28 |
29 |
30 |
31 |
32 |
33 | >
34 |
35 |
36 |
37 | );
38 | }
39 |
40 | export default winstall;
41 |
--------------------------------------------------------------------------------
/pages/_document.js:
--------------------------------------------------------------------------------
1 | import Document, { Html, Head, Main, NextScript } from "next/document";
2 |
3 | class MyDocument extends Document {
4 | static async getInitialProps(ctx) {
5 | const initialProps = await Document.getInitialProps(ctx);
6 | return { ...initialProps };
7 | }
8 |
9 | render() {
10 | return (
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
23 |
27 |
38 |
39 | );
40 | }
41 | }
42 |
43 | export default MyDocument;
44 |
--------------------------------------------------------------------------------
/pages/api/auth/[...nextauth].js:
--------------------------------------------------------------------------------
1 | import NextAuth from "next-auth";
2 | import TwitterProvider from "next-auth/providers/twitter";
3 |
4 | const options = {
5 | providers: [
6 | TwitterProvider({
7 | clientId: process.env.TWITTER_ID,
8 | clientSecret: process.env.TWITTER_SECRET,
9 | }),
10 | ],
11 |
12 | secret: process.env.SECRET,
13 |
14 | session: {
15 | strategy: "jwt",
16 | maxAge: 30 * 24 * 60 * 60,
17 | },
18 |
19 | callbacks: {
20 | session: async ({ session, token }) => {
21 | if (session && token) {
22 | session.user = token;
23 | }
24 |
25 | return Promise.resolve(session);
26 | },
27 |
28 | jwt: async ({ token, account, profile }) => {
29 | if (profile) {
30 | token.id = profile.id_str;
31 | }
32 |
33 | if (account) {
34 | token.accessToken = account.oauth_token;
35 | token.refreshToken = account.oauth_token_secret;
36 | }
37 |
38 | return Promise.resolve(token);
39 | },
40 | },
41 | };
42 |
43 | export default (req, res) => NextAuth(req, res, options);
44 |
--------------------------------------------------------------------------------
/pages/api/twitter.js:
--------------------------------------------------------------------------------
1 | export const callTwitterAPI = async (endpoint, method="GET") => {
2 | let response, error;
3 |
4 | try {
5 | const res = await fetch(endpoint, {
6 | method: method,
7 | headers: {
8 | "Authorization": `Bearer ${process.env.NEXT_PUBLIC_TWITTER_BEARER}`
9 | }
10 | });
11 |
12 | if(res.status !== 200) {
13 | error = await res.json();
14 | } else{
15 | response = await res.json();
16 | }
17 | } catch(err){
18 | error = err.message;
19 | }
20 |
21 | return { response, error }
22 | }
23 |
24 | export default async function handler(req, res) {
25 | const { response, error } = await callTwitterAPI(req.headers.endpoint, req.method);
26 |
27 | return res.send({ response, error });
28 | }
--------------------------------------------------------------------------------
/pages/apps.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import styles from "../styles/apps.module.scss";
3 |
4 | import SingleApp from "../components/SingleApp";
5 | import Footer from "../components/Footer";
6 | import { ListSort, applySort } from "../components/ListSort";
7 | import MetaTags from "../components/MetaTags";
8 | import Search from "../components/Search";
9 |
10 | import {
11 | FiChevronLeft,
12 | FiChevronRight,
13 | FiArrowLeftCircle,
14 | FiArrowRightCircle,
15 | } from "react-icons/fi";
16 |
17 | import Router from "next/router";
18 | import fetchWinstallAPI from "../utils/fetchWinstallAPI";
19 | import Error from "../components/Error";
20 | import DonateCard from "../components/DonateCard";
21 |
22 | function Store({ data, error }) {
23 | if (error) return ;
24 |
25 | const [apps, setApps] = useState([]);
26 | const [searchInput, setSearchInput] = useState();
27 | const [offset, setOffset] = useState(0);
28 | const [sort, setSort] = useState();
29 |
30 | const appsPerPage = 60;
31 |
32 | const totalPages = Math.ceil(apps.length / appsPerPage);
33 |
34 | useEffect(() => {
35 | // Default to showing most recently updated first to entice Google to index
36 | // them, and to demonstrate to users that the site is being kept up-to-date.
37 | let sortOrder = Router.query.sort || "update-desc";
38 | applySort(data, sortOrder);
39 | setSort(sortOrder);
40 | setApps(data);
41 |
42 | let handlePagination = (e) => {
43 | if (e.keyCode === 39) {
44 | let nextBtn = document.getElementById("next");
45 |
46 | if (nextBtn) {
47 | document.getElementById("next").click();
48 | }
49 | } else if (e.keyCode === 37) {
50 | let previousBtn = document.getElementById("previous");
51 |
52 | if (previousBtn) {
53 | document.getElementById("previous").click();
54 | }
55 | }
56 | };
57 |
58 | document.addEventListener("keydown", handlePagination);
59 |
60 | setPagination(apps.length);
61 |
62 | return () => document.removeEventListener("keydown", handlePagination);
63 | // eslint-disable-next-line react-hooks/exhaustive-deps
64 | }, []);
65 |
66 | const setPagination = (appCount) => {
67 | let requestedPage = parseInt(Router.query.page);
68 | if (requestedPage) {
69 | let maxPages = Math.round(appCount / appsPerPage) + 1;
70 |
71 | // we check if its a valid page number
72 | if (requestedPage > maxPages || requestedPage < 2) return;
73 |
74 | // if it is, we continue
75 | let calculateOffset = appsPerPage * (requestedPage - 1);
76 | setOffset(calculateOffset);
77 | }
78 | };
79 |
80 | let handleNext = () => {
81 | window.scrollTo(0, 0);
82 | setOffset((offset) => offset + appsPerPage);
83 |
84 | Router.replace({
85 | pathname: "/apps",
86 | query: {
87 | page: Math.round((offset + appsPerPage - 1) / appsPerPage) + 1,
88 | },
89 | });
90 | };
91 |
92 | let handlePrevious = () => {
93 | window.scrollTo(0, 0);
94 | setOffset((offset) => offset - appsPerPage);
95 |
96 | Router.replace({
97 | pathname: "/apps",
98 | query: {
99 | page: Math.round((offset + appsPerPage - 1) / appsPerPage) - 1,
100 | },
101 | });
102 | };
103 |
104 | let Pagination = ({ small, disable }) => {
105 | return (
106 |
107 | 0 ? (disable ? "disabled" : null) : "disabled"}
113 | >
114 |
115 | {!small ? "Previous" : ""}
116 |
117 |
130 | {!small ? "Next" : ""}
131 |
132 |
133 |
134 | );
135 | };
136 |
137 | const Title = () => {
138 | return (
139 | <>
140 | {!searchInput && All apps {`(${apps.length})`} }
141 | {searchInput && (
142 | <>
143 | {searchInput.startsWith("tags: ") && (
144 | Tag: {searchInput.split(": ")[1]}
145 | )}
146 | {!searchInput.startsWith("tags: ") && Search results }
147 | >
148 | )}
149 | >
150 | );
151 | };
152 |
153 | if (!apps) return <>>;
154 |
155 | return (
156 |
157 |
158 |
159 |
164 |
165 |
setSearchInput(q)}
168 | label={"Search for apps"}
169 | placeholder={"Enter you search term here"}
170 | />
171 |
172 |
173 | {!searchInput && (
174 | <>
175 |
176 | Showing {apps.slice(offset, offset + appsPerPage).length} apps
177 | (page {Math.round((offset + appsPerPage - 1) / appsPerPage)} of{" "}
178 | {totalPages}).
179 |
180 |
setSort(sort)}
184 | />
185 | >
186 | )}
187 |
188 |
189 | {!searchInput && (
190 |
191 | {apps.slice(offset, offset + appsPerPage).map((app, index) => (
192 |
193 |
197 |
198 | {index % 15 === 0 && }
199 |
200 | ))}
201 |
202 | )}
203 |
204 |
205 |
206 |
207 | Hit the and keys on your
208 | keyboard to navigate between pages quickly.
209 |
210 |
211 |
212 |
213 |
214 | );
215 | }
216 |
217 | export async function getStaticProps() {
218 | let { response: apps, error } = await fetchWinstallAPI(`/apps`, {}, true);
219 |
220 | if (error) return { props: { error } };
221 |
222 | return {
223 | props: {
224 | data: apps ?? null,
225 | },
226 | };
227 | }
228 |
229 | export default Store;
230 |
--------------------------------------------------------------------------------
/pages/apps/[id].js:
--------------------------------------------------------------------------------
1 | import styles from "../../styles/home.module.scss";
2 |
3 | import SingleApp from "../../components/SingleApp";
4 |
5 | import Footer from "../../components/Footer";
6 | import Error from "../../components/Error";
7 |
8 | import Skeleton from "react-loading-skeleton";
9 |
10 | import { useRouter } from "next/router";
11 | import MetaTags from "../../components/MetaTags";
12 | import fetchWinstallAPI from "../../utils/fetchWinstallAPI";
13 | import DonateCard from "../../components/DonateCard";
14 |
15 | function AppSkeleton() {
16 | return (
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | );
31 | }
32 |
33 | function AppDetail({ app, popular}) {
34 | const router = useRouter();
35 | const fallbackMessage = {
36 | title: "Sorry! We could not load this app.",
37 | subtitle: "Unfortunately, this app could not be loaded. Either it does not exist, or something else went wrong. Please try again later."
38 | }
39 |
40 | if(!router.isFallback && !app){
41 | return
42 | }
43 |
44 | return (
45 |
46 |
47 |
48 | {router.isFallback ? (
49 |
50 | ) : (
51 |
59 | )}
60 |
61 |
66 |
67 |
68 |
69 |
70 |
71 |
72 | );
73 | }
74 |
75 | export async function getStaticPaths() {
76 | return {
77 | paths: [],
78 | fallback: true,
79 | };
80 | }
81 |
82 | export async function getStaticProps({ params }) {
83 | try{
84 | let { response: app } = await fetchWinstallAPI(`/apps/${params.id}`);
85 |
86 | return { props: app ? { app } : {} }
87 | } catch(err) {
88 | return { props: {} };
89 | }
90 | }
91 |
92 | export default AppDetail;
--------------------------------------------------------------------------------
/pages/eli5.js:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import MetaTags from "../components/MetaTags";
3 | import Footer from "../components/Footer";
4 |
5 | export default function Explainer() {
6 | return (
7 |
8 |
9 |
10 |
11 | How does this app work?
12 |
13 | winstall is powered by Windows Package Manager (aka "winget"),{" "}
14 |
19 | Microsoft's new package manager
20 | {" "}
21 | for Windows 10. Windows Package Manager is currently in preview, and
22 | it is not available by default in Windows 10.
23 |
24 |
25 |
26 |
27 | Installing Windows Package Manager
28 |
29 | If you don't already have Windows Package Manager, you can install
30 | it by downloading and installing the{" "}
31 |
36 | latest .appxbundle file from here.
37 |
38 |
39 |
40 |
41 |
42 | Using winstall
43 |
44 | To use winstall, you can search for apps on the homepage.
45 | Additionally, you can also view all the apps available via Windows
46 | Package Manager{" "}
47 |
48 | on this page.
49 |
50 |
51 |
52 | Simply select the apps you want to download and click on the
53 | "Generate Script" button at the bottom of the screen. You will then
54 | be presented with a command that you can copy and paste into any
55 | Windows command-line. Input that into a command line app of your
56 | choice, and hit enter to start installing the apps one-by-one using
57 | Windows Package Manager. You can also generate a PowerShell script by toggling
58 | the "Show Powershell script" option.
59 |
60 |
61 | Alternatively, you can click on the "Download .bat/.ps1" button which
62 | will download a batch file. However, you will likely get a security
63 | warning from your browser. In that case, ignore the warning as the
64 | batch file is completely secure. Once downloaded, you can
65 | double-click the .bat/ file to install the apps using the Windows
66 | Package Manager.
67 |
68 |
69 |
70 |
71 | How is the data obtained?
72 |
73 | winstall is powered by an API that I have built. The API regularly checks Microsoft's {" "}
74 |
79 | official repository
80 | {" "}
81 | for Windows Package Manager apps. This means it always provides the latest data.
82 |
83 | The API updates its data every 15 minutes on weekdays, and every 3 hours on weekends. I will be making the API open-source in the near-future.
84 |
85 |
86 |
109 |
110 |
111 | Project history
112 |
113 | winstall was originally created by
114 | {" "}
119 | Mehedi Hassan
120 | {" "}
121 | as a side project, but is now owned and maintained by{" "}
122 |
126 | winget.Pro
127 | {" "}
128 | - with winget.Pro, you can have your own, securely hosted repositories for the Windows Package Manager. Our private winget repository gives you better control over who receives your software.
129 |
130 |
131 |
132 |
133 |
134 |
135 | );
136 | }
137 |
--------------------------------------------------------------------------------
/pages/generate.js:
--------------------------------------------------------------------------------
1 | import { useContext, useEffect, useState } from "react";
2 | import Link from "next/link";
3 |
4 | import styles from "../styles/home.module.scss";
5 |
6 | import ListPackages from "../components/ListPackages";
7 | import SingleApp from "../components/SingleApp";
8 | import SelectedContext from "../ctx/SelectedContext";
9 |
10 | import Footer from "../components/Footer";
11 |
12 | import { FiHome } from "react-icons/fi";
13 | import MetaTags from "../components/MetaTags";
14 | import ExportApps from "../components/AppExport/ExportApps";
15 |
16 | function Generate() {
17 | const { selectedApps } = useContext(SelectedContext);
18 | const [apps, setApps] = useState([]);
19 |
20 | useEffect(() => {
21 | setApps(selectedApps);
22 | }, [ apps, selectedApps ]);
23 |
24 | if(selectedApps.length === 0){
25 | return (
26 |
27 |
28 |
29 |
30 |
Your don't have any apps selected.
31 |
32 | Make sure you select some apps first to be able to generate a
33 | script :)
34 |
35 |
36 |
37 |
38 | Go home
39 |
40 |
41 |
42 |
43 |
48 |
49 |
50 |
51 | );
52 | }
53 |
54 | return (
55 |
56 |
57 |
58 |
59 |
Your apps are ready to be installed.
60 | Make sure you have Windows Package Manager installed :)
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
Apps you are downloading ({selectedApps.length})
71 |
72 | {selectedApps.map((app) => (
73 |
74 | ))}
75 |
76 |
77 |
78 |
79 |
80 | );
81 | }
82 |
83 | export default Generate;
84 |
--------------------------------------------------------------------------------
/pages/index.js:
--------------------------------------------------------------------------------
1 | import styles from "../styles/home.module.scss";
2 |
3 | import Search from "../components/Search";
4 | import PopularApps from "../components/PopularApps";
5 | import MetaTags from "../components/MetaTags";
6 | import Recommendations from "../components/Recommendations";
7 |
8 | import Footer from "../components/Footer";
9 | import { shuffleArray } from "../utils/helpers";
10 | import popularAppsList from "../data/popularApps.json";
11 | import FeaturePromoter from "../components/FeaturePromoter";
12 | import Link from "next/link";
13 | import { FiPlus, FiPackage } from "react-icons/fi";
14 | import fetchWinstallAPI from "../utils/fetchWinstallAPI";
15 | import Error from "../components/Error";
16 | import DonateCard from "../components/DonateCard";
17 |
18 | function Home({ popular, apps, recommended, error}) {
19 | if(error) {
20 | return
21 | }
22 |
23 | return (
24 |
25 |
26 |
27 |
28 |
29 |
30 | Browse the winget repository.
31 |
32 |
33 | Install Windows apps quickly with Windows Package Manager.
34 |
35 |
36 |
37 |
38 |
39 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | {/*
*/}
56 |
57 |
58 | Introducing Packs
59 | Curate and share the apps you use daily.
60 |
61 | Create a pack
62 | View packs
63 |
64 |
65 |
66 |
67 |
68 | );
69 | }
70 |
71 | export async function getStaticProps(){
72 | let popular = shuffleArray(Object.values(popularAppsList));
73 |
74 | let { response: apps, error: appsError } = await fetchWinstallAPI(`/apps`);
75 | let { response: recommended, error: recommendedError } = await fetchWinstallAPI(`/packs/users/${process.env.NEXT_OFFICIAL_PACKS_CREATOR}`);
76 |
77 | if(appsError) console.error(appsError);
78 | if(recommendedError) console.error(recommendedError);
79 |
80 | if(appsError || recommendedError) return { props: { error: `Could not fetch data from Winstall API.`} };
81 |
82 |
83 | // get all apps with id -> filter popular apps for those with an id
84 | const appsWithId = new Set(Object.values(apps).map((x) => x._id))
85 | popular = popular.filter((a) => appsWithId.has(a._id))
86 |
87 | // get the new pack data, and versions data, etc.
88 | const getPackData = recommended.map(async (pack) => {
89 | return new Promise(async(resolve) => {
90 | const appsList = pack.apps;
91 |
92 | const getIndividualApps = appsList.map(async (app, index) => {
93 | return new Promise(async (resolve) => {
94 | let { response: appData, error } = await fetchWinstallAPI(`/apps/${app._id}`);
95 |
96 | if(error) appData = null;
97 |
98 | appsList[index] = appData;
99 | resolve();
100 | })
101 | })
102 |
103 | await Promise.all(getIndividualApps).then(() => {
104 | pack.apps = appsList.filter(app => app != null);
105 | resolve();
106 | })
107 | })
108 | })
109 |
110 | await Promise.all(getPackData);
111 |
112 | return (
113 | {
114 | props: {
115 | popular,
116 | apps,
117 | recommended
118 | }
119 | }
120 | )
121 | }
122 |
123 | export default Home;
124 |
--------------------------------------------------------------------------------
/pages/packs/[id].js:
--------------------------------------------------------------------------------
1 | import styles from "../../styles/packPage.module.scss";
2 |
3 | import Error from "../../components/Error";
4 |
5 | import Skeleton from "react-loading-skeleton";
6 | import Link from "next/link";
7 | import { useRouter } from "next/router";
8 | import MetaTags from "../../components/MetaTags";
9 | import { useEffect, useState, useContext } from "react";
10 | import PageWrapper from "../../components/PageWrapper";
11 | import PackAppsList from "../../components/PackAppsList";
12 | import SelectedContext from "../../ctx/SelectedContext";
13 | import { timeAgo } from "../../utils/helpers";
14 | import {
15 | FiCodepen,
16 | FiPackage,
17 | FiShare2,
18 | FiClock,
19 | FiEdit,
20 | FiTrash,
21 | } from "react-icons/fi";
22 | import { getSession } from "next-auth/react";
23 | import fetchWinstallAPI from "../../utils/fetchWinstallAPI";
24 | import { callTwitterAPI } from "../api/twitter";
25 | import ExportApps from "../../components/AppExport/ExportApps";
26 |
27 | function AppSkeleton() {
28 | return (
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | );
48 | }
49 |
50 | function ScriptCode({ apps }) {
51 | const [copyText, setCopyText] = useState("Copy to clipboard");
52 | const [script, setScript] = useState("");
53 | const [showPS, setShowPS] = useState(false);
54 |
55 | let handleScriptChange = () => {
56 | let installs = [];
57 |
58 | apps.map((app) => {
59 | installs.push(
60 | `winget install --id=${app._id} ${app.selectedVersion !== app.latestVersion
61 | ? `-v "${app.selectedVersion}" `
62 | : ""
63 | }-e`
64 | );
65 |
66 | return app;
67 | });
68 |
69 | let newScript = installs.join(showPS ? " ; " : " && ");
70 |
71 | if (script !== newScript) {
72 | setCopyText("Copy to clipboard");
73 | }
74 |
75 | setScript(newScript);
76 | };
77 |
78 | useEffect(() => {
79 | handleScriptChange();
80 | }, [handleScriptChange]);
81 |
82 | let handleCopy = () => {
83 | navigator.clipboard
84 | .writeText(script)
85 | .then(() => setCopyText("Copied!"))
86 | .catch((err) => {
87 | document.querySelector("textarea").select();
88 | });
89 | };
90 |
91 | let handleBat = () => {
92 | let dl = document.querySelector("#gsc");
93 | dl.setAttribute("download", `winstall${showPS ? ".ps1" : ".bat"}`);
94 | dl.href = "data:text/plain;base64," + btoa(script);
95 | dl.click();
96 | };
97 |
98 | let handleScriptSwitch = () => {
99 | setShowPS(!showPS);
100 |
101 | if (!showPS) {
102 | setScript(script.replace(/&&/g, ";"));
103 | } else {
104 | setScript(script.replace(/;/g, "&&"));
105 | }
106 |
107 | setCopyText("Copy to clipboard");
108 | };
109 |
110 | return (
111 |
112 |
117 |
118 | );
119 | }
120 |
121 | function PackDetail({ pack, creator, error }) {
122 | const router = useRouter();
123 | const { selectedApps, setSelectedApps } = useContext(SelectedContext);
124 | const [user, setUser] = useState();
125 | const [deleting, setDeleting] = useState(false);
126 | const [deleteLabel, setDeleteLabel] = useState("Delete Pack");
127 |
128 | useEffect(() => {
129 | getSession().then(async (session) => {
130 | if (!session) {
131 | return;
132 | }
133 |
134 | setUser(session.user);
135 | });
136 | }, []);
137 |
138 | const fallbackMessage = {
139 | title: "Sorry! We could not load this pack.",
140 | subtitle: error
141 | ? `Recieved error: ${error}`
142 | : "Unfortunately, this pack could not be loaded. Either it does not exist, or something else went wrong. Please try again later.",
143 | };
144 |
145 | if (!router.isFallback && !pack) {
146 | return ;
147 | }
148 |
149 | const handleSelectAll = () => {
150 | const updatedList = [...selectedApps, ...pack.apps];
151 |
152 | let uniqueList = [
153 | ...new Map(updatedList.map((item) => [item["_id"], item])).values(),
154 | ];
155 |
156 | setSelectedApps(uniqueList);
157 | };
158 |
159 | const handleShare = () => {
160 | const link = `https://twitter.com/intent/tweet?text=${encodeURIComponent(
161 | `Checkout the "${pack.title}" pack by @${creator.screen_name}!`
162 | )}&url=${encodeURIComponent(
163 | `https://winstall.app/packs/${pack._id}`
164 | )}&via=winstallHQ`;
165 |
166 | window.open(link);
167 | };
168 |
169 | const deletePack = async () => {
170 | if (!user) return;
171 |
172 | setDeleting(true);
173 | setDeleteLabel("Deleting...");
174 |
175 | const { response } = await fetchWinstallAPI(`/packs/${pack._id}`, {
176 | method: "DELETE",
177 | headers: {
178 | Authorization: `${user.accessToken},${user.refreshToken}`,
179 | "Content-Type": "application/json",
180 | },
181 | body: JSON.stringify({ creator: pack.creator }),
182 | });
183 |
184 | if (response && response.msg) {
185 | setDeleteLabel("Deleted!");
186 | localStorage.removeItem("ownPacks");
187 | router.push("/packs");
188 | }
189 | };
190 |
191 | const handleDelete = async (e) => {
192 | if (deleting) return;
193 |
194 | if ("confirm" in window && typeof window.confirm === "function") {
195 | if (window.confirm("Are you sure you want to delete this pack?")) {
196 | deletePack();
197 | }
198 | } else {
199 | deletePack();
200 | }
201 | };
202 |
203 | return (
204 |
205 |
206 | {router.isFallback ? (
207 |
208 | ) : (
209 |
210 |
214 |
215 |
{pack.title}
216 |
217 |
222 |
226 |
230 | @{creator.screen_name}
231 |
232 |
233 |
234 |
{pack.desc}
235 |
236 | Last updated {timeAgo(pack.updatedAt)}{" "}
237 |
238 |
239 |
254 |
255 | {user && user.id === pack.creator && (
256 |
267 | )}
268 |
269 |
270 |
271 |
272 |
273 | )}
274 |
275 | {/*
*/}
276 |
277 |
278 | );
279 | }
280 |
281 | export async function getStaticPaths() {
282 | return {
283 | paths: [],
284 | fallback: true,
285 | };
286 | }
287 |
288 | export async function getStaticProps({ params }) {
289 | try {
290 | let { response: pack } = await fetchWinstallAPI(`/packs/${params.id}`);
291 | const { response: creator, error } = await callTwitterAPI(
292 | `https://api.twitter.com/2/users/${pack.creator}`
293 | );
294 |
295 | if (error)
296 | return {
297 | props: {
298 | error:
299 | error.errors.length > 0
300 | ? error.errors[0].message
301 | : "Could not get user data.",
302 | },
303 | };
304 |
305 | let appsList = pack.apps;
306 |
307 | const getIndividualApps = appsList.map(async (app, index) => {
308 | return new Promise(async (resolve) => {
309 |
310 | let { response: appData, error } = await fetchWinstallAPI(
311 | `/apps/${app._id}`
312 | );
313 |
314 |
315 | if (error) appData = null;
316 |
317 | appsList[index] = appData;
318 | resolve();
319 | });
320 | });
321 |
322 | await Promise.all(getIndividualApps).then(() => {
323 | pack.apps = appsList.filter((app) => app != null);
324 | });
325 |
326 | return { props: pack ? { pack, creator } : {}, revalidate: 600 };
327 | } catch (error) {
328 | return { props: { error } };
329 | }
330 | }
331 |
332 | export default PackDetail;
333 |
--------------------------------------------------------------------------------
/pages/packs/create.js:
--------------------------------------------------------------------------------
1 | import { useContext, useState, useEffect } from "react";
2 | import { getSession, signIn } from "next-auth/react";
3 |
4 | import styles from "../../styles/create.module.scss";
5 |
6 | import PackAppsList from "../../components/PackAppsList";
7 | import SelectedContext from "../../ctx/SelectedContext";
8 |
9 | import PageWrapper from "../../components/PageWrapper";
10 |
11 | import { FiTwitter } from "react-icons/fi";
12 | import MetaTags from "../../components/MetaTags";
13 | import CreatePackForm from "../../components/CreatePackForm";
14 | import FeaturePromoter from "../../components/FeaturePromoter";
15 | import fetchWinstallAPI from "../../utils/fetchWinstallAPI";
16 |
17 | function Create({ allApps }) {
18 | const { selectedApps, setSelectedApps } = useContext(SelectedContext);
19 | const [user, setUser] = useState();
20 | const [packApps, setPackApps] = useState([]);
21 |
22 | useEffect(() => {
23 | setPackApps(selectedApps);
24 |
25 | const restoreBackup = async () => {
26 | const checkForBackup = await localStorage.getItem("winstallLogin");
27 |
28 | const backup = JSON.parse(checkForBackup);
29 |
30 | if (!backup) return;
31 |
32 | setSelectedApps(backup);
33 | setPackApps(backup);
34 |
35 | await localStorage.removeItem("winstallLogin");
36 | };
37 |
38 | restoreBackup();
39 |
40 | getSession().then(async (session) => {
41 | if (!session) return;
42 |
43 | if (session.user) setUser(session.user);
44 |
45 | setSelectedApps([]);
46 | });
47 | }, []);
48 |
49 | const handleLogin = async () => {
50 | const appsBackup = JSON.stringify(selectedApps);
51 |
52 | await localStorage.setItem("winstallLogin", appsBackup);
53 |
54 | signIn("twitter");
55 | };
56 |
57 | const updatePackApps = (apps) => {
58 | setPackApps(apps);
59 | };
60 |
61 | if (!user) {
62 | return (
63 |
64 |
65 |
66 | One more thing...
67 | Welcome! Login with Twitter to be able to create a pack.
68 |
69 |
70 |
71 | Login
72 |
73 |
74 |
75 |
76 | );
77 | }
78 |
79 | return (
80 |
81 |
82 |
83 |
84 |
Create a pack
85 |
86 | {user && (
87 | <>
88 |
89 |
90 |
91 |
92 |
99 | >
100 | )}
101 |
102 |
103 | );
104 | }
105 |
106 | export async function getStaticProps() {
107 | let { response: apps } = await fetchWinstallAPI(`/apps`);
108 |
109 | return {
110 | props: {
111 | allApps: apps ?? null,
112 | },
113 | };
114 | }
115 |
116 | export default Create;
117 |
--------------------------------------------------------------------------------
/pages/packs/edit.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import { getSession } from "next-auth/react";
3 | import Error from "../../components/Error";
4 | import PageWrapper from "../../components/PageWrapper";
5 | import MetaTags from "../../components/MetaTags";
6 | import styles from "../../styles/create.module.scss";
7 | import Router from "next/router";
8 | import CreatePackForm from "../../components/CreatePackForm";
9 | import PackAppsList from "../../components/PackAppsList";
10 | import fetchWinstallAPI from "../../utils/fetchWinstallAPI";
11 |
12 | export default function Edit({ allApps }) {
13 | const [user, setUser] = useState();
14 | const [loading, setLoading] = useState(true);
15 | const [packId, setPackId] = useState();
16 | const [pack, setPack] = useState();
17 | const [notFound, setNotFound] = useState(false);
18 | const [accessDenied, denyAccess] = useState(false);
19 | const [packApps, setPackApps] = useState([]);
20 |
21 | useEffect(() => {
22 | getSession().then(async (session) => {
23 | if (session && session.user) setUser(session.user);
24 |
25 | setPackId(Router.query.id);
26 |
27 | if (session && session.user && Router.query.id) {
28 | checkPack(Router.query.id, session.user.id);
29 | } else {
30 | setLoading(false);
31 | }
32 | });
33 | }, []);
34 |
35 | const checkPack = async (id, userId) => {
36 | let { response: pack, error } = await fetchWinstallAPI(`/packs/${id}`);
37 |
38 | if (!pack) {
39 | setNotFound(true);
40 | setLoading(false);
41 | return;
42 | }
43 |
44 | if (pack.creator === userId) {
45 | const appsList = pack.apps;
46 |
47 | const getIndividualApps = appsList.map(async (app, index) => {
48 | return new Promise(async (resolve) => {
49 | let { response: appData, error } = await fetchWinstallAPI(
50 | `/apps/${app._id}`
51 | );
52 |
53 | if (error) appData = null;
54 |
55 | appsList[index] = appData;
56 | resolve();
57 | });
58 | });
59 |
60 | await Promise.all(getIndividualApps).then(() => {
61 | pack.apps = appsList.filter((app) => app != null);
62 | });
63 |
64 | setPack(pack);
65 | setPackApps(pack.apps);
66 | setLoading(false);
67 | } else {
68 | setLoading(false);
69 | denyAccess(true);
70 | }
71 | };
72 |
73 | if (loading)
74 | return (
75 |
76 | Loading...
77 |
78 | );
79 |
80 | // If no session exists, display access denied message
81 | if (!user)
82 | return (
83 |
87 | );
88 |
89 | // If no pack ID query, return error
90 | if (!packId || notFound)
91 | return (
92 |
96 | );
97 |
98 | // If pack isn't owned by this user
99 | if (accessDenied)
100 | return (
101 |
105 | );
106 |
107 | // If everything passes, let them edit
108 | if (!pack) return <>>;
109 |
110 | const updatePackApps = (apps) => {
111 | setPackApps(apps);
112 | };
113 |
114 | return (
115 |
116 |
117 |
118 |
119 |
Edit Pack
120 |
121 | {/*
122 |
123 | */}
124 |
125 |
137 |
138 | {/*
Note: it may take up to 10 minutes for your changes to apply for you and others on the site.
*/}
139 |
140 |
141 |
142 |
143 |
150 |
151 |
152 | );
153 | }
154 |
155 | export async function getStaticProps() {
156 | let { response: apps } = await fetchWinstallAPI(`/apps`);
157 |
158 | return {
159 | props: {
160 | allApps: apps ?? null,
161 | },
162 | };
163 | }
164 |
--------------------------------------------------------------------------------
/pages/packs/index.js:
--------------------------------------------------------------------------------
1 | import PageWrapper from "../../components/PageWrapper";
2 | import MetaTags from "../../components/MetaTags";
3 | import styles from "../../styles/apps.module.scss";
4 | import PackPreview from "../../components/PackPreview";
5 | import Link from "next/link";
6 | import { FiChevronLeft, FiPlus, FiChevronRight, FiArrowLeftCircle, FiArrowRightCircle } from "react-icons/fi";
7 | import FeaturePromoter from "../../components/FeaturePromoter";
8 | import React, { useState } from "react";
9 | import fetchWinstallAPI from "../../utils/fetchWinstallAPI";
10 | import Error from "../../components/Error";
11 | import DonateCard from "../../components/DonateCard";
12 |
13 | export default function Packs({ packs, error }) {
14 | if (error) return
15 |
16 | const [offset, setOffset] = useState(0);
17 |
18 | const itemsPerPage = 60;
19 | const totalPages = Math.ceil(packs.length / itemsPerPage);
20 |
21 | let handleNext = () => {
22 | window.scrollTo(0, 0)
23 | setOffset(offset => offset + itemsPerPage);
24 | }
25 |
26 | let handlePrevious = () => {
27 | window.scrollTo(0, 0)
28 | setOffset(offset => offset - itemsPerPage);
29 | }
30 |
31 | const Pagination = ({ small, disable }) => {
32 | return (
33 |
34 | 0 ? (disable ? "disabled" : null) : "disabled"}
40 | >
41 |
42 | {!small ? "Previous" : ""}
43 |
44 |
51 | {!small ? "Next" : ""}
52 |
53 |
54 |
55 | );
56 | }
57 |
58 |
59 | return (
60 |
61 |
62 |
63 |
64 |
65 | Introducing Packs
66 | Curate and share the apps you use daily.
67 |
68 | Create a pack
69 |
70 |
71 |
72 |
73 |
74 |
All packs {`(${packs.length})`}
75 |
76 | Showing {packs.slice(offset, offset + itemsPerPage).length} packs
77 | (page {Math.round((offset + itemsPerPage - 1) / itemsPerPage)} of{" "}
78 | {totalPages}).
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 | {packs.slice(offset, offset + itemsPerPage).map((pack, index) => (
88 |
89 |
90 |
91 |
92 | {index % 15 === 0 && }
93 |
94 | ))}
95 |
96 |
97 |
98 |
99 |
100 | Hit the and keys
101 | on your keyboard to navigate between pages quickly.
102 |
103 |
104 |
105 |
106 |
107 | )
108 | }
109 |
110 |
111 | export async function getStaticProps() {
112 | let { response: packs, error } = await fetchWinstallAPI(`/packs`, {}, true);
113 |
114 | if (error) {
115 | console.error(error);
116 | return { props: { error } };
117 | }
118 |
119 | const officialPacks = process.env.NEXT_OFFICIAL_PACKS_CREATOR;
120 |
121 | packs = packs.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
122 | packs = packs.sort((a, b) => a.creator === officialPacks ? -1 : 1)
123 |
124 | return {
125 | props: {
126 | packs,
127 | },
128 | revalidate: 600
129 | };
130 | }
--------------------------------------------------------------------------------
/pages/privacy.js:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import MetaTags from "../components/MetaTags";
3 | import Footer from "../components/Footer";
4 |
5 | export default function Privacy() {
6 | return (
7 |
8 |
9 |
10 |
11 | Privacy Policy
12 | Your privacy is important to us. winstall's policy is to respect your privacy regarding any information we may collect from you when using our app, and other sites we own and operate.
13 |
14 | We don’t collect or share any personally identifying information publicly or with third-parties. What data we store, we’ll protect within commercially acceptable means to prevent loss and theft, as well as unauthorised access, disclosure, copying, use or modification. We only collect analytics data from the use of our app, and we will never collect any personal data.
15 |
16 | When you login to our app using Twitter, we do not collect or store any of your Twitter profile data. winstall only stores your Twitter user ID when you create/publish a "Pack". winstall will never collect your personal Twitter profile data.
17 |
18 | We collect analytics data by fair and lawful means, with your knowledge and consent. We use Google Analytics to track app analytics, and the analytics data collected is never shared with a third-party. The data is ONLY collected to track the app's usage metrics. Please refer to Google Analytics' privacy policy for more information.
19 |
20 | Our app may link to external sites that are not operated by us. Please be aware that we have no control over the content and practices of these sites, and cannot accept responsibility or liability for their respective privacy policies.
21 |
22 | Your continued use of our website and app will be regarded as acceptance of our practices around privacy. If you have any questions about how we handle user data feel free to contact us at products@builtbymeh.com.
23 |
24 | winstall is not associated with Microsoft, Windows, or Windows Package Manager.
25 |
26 | This policy is effective as of September 9, 2020.
27 |
28 |
29 |
30 |
31 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/pages/sitemap.xml.js:
--------------------------------------------------------------------------------
1 | import fetchWinstallAPI from "../utils/fetchWinstallAPI";
2 |
3 | function generateSiteMap(urlPrefix, apps, packs, users) {
4 | return `
5 |
6 | ${['apps', 'packs', 'eli5', 'privacy']
7 | .map((page) => {
8 | return `
9 |
10 | ${urlPrefix}/${page}
11 |
12 | `;
13 | })
14 | .join('')}
15 | ${apps
16 | .map(({ _id, updatedAt }) => {
17 | return `
18 |
19 | ${urlPrefix}/apps/${escapeXml(_id)}
20 | ${updatedAt}
21 |
22 | `;
23 | })
24 | .join('')}
25 | ${packs
26 | .map(({ _id, updatedAt }) => {
27 | return `
28 |
29 | ${urlPrefix}/packs/${_id}
30 | ${updatedAt}
31 |
32 | `;
33 | })
34 | .join('')}
35 | ${users
36 | .map(( id ) => {
37 | return `
38 |
39 | ${urlPrefix}/users/${id}
40 |
41 | `;
42 | })
43 | .join('')}
44 |
45 | `;
46 | }
47 |
48 | const escapeXml = (str) => {
49 | return str.replace(/[&<>"']/g, (char) => {
50 | switch (char) {
51 | case '&': return '&';
52 | case '<': return '<';
53 | case '>': return '>';
54 | case '"': return '"';
55 | case "'": return ''';
56 | default: return char;
57 | }
58 | });
59 | };
60 |
61 | function SiteMap() {
62 | }
63 |
64 | export async function getServerSideProps({ req, res }) {
65 | const protocol = req.headers['x-forwarded-proto'] || 'http';
66 | const host = req.headers['host'];
67 | const urlPrefix = protocol + "://" + host;
68 |
69 | let { response: apps, err1 } = await fetchWinstallAPI(`/apps`, {}, true);
70 | if (err1) return { props: { err1 } };
71 |
72 | let { response: packs, err2 } = await fetchWinstallAPI(`/packs`, {}, true);
73 | if (err2) return { props: { err2 } };
74 |
75 | const users = Array.from(new Set(packs.map(pack => pack.creator)));
76 | users.forEach(pack => {console.log(pack);});
77 |
78 | const sitemap = generateSiteMap(urlPrefix, apps, packs, users);
79 |
80 | res.setHeader('Content-Type', 'text/xml');
81 | res.write(sitemap);
82 | res.end();
83 |
84 | return {
85 | props: {},
86 | };
87 | }
88 |
89 | export default SiteMap;
--------------------------------------------------------------------------------
/pages/users/[id].js:
--------------------------------------------------------------------------------
1 | import PageWrapper from "../../components/PageWrapper";
2 | import MetaTags from "../../components/MetaTags";
3 | import styles from "../../styles/apps.module.scss";
4 | import PackPreview from "../../components/PackPreview";
5 | import { useEffect, useState } from "react";
6 | import { getSession } from "next-auth/react";
7 | import Error from "../../components/Error";
8 | import fetchWinstallAPI from "../../utils/fetchWinstallAPI";
9 |
10 | function UserProfile({ uid }) {
11 | const [user, setUser] = useState();
12 | const [title, setTitle] = useState("Loading...");
13 | const [packs, setPacks] = useState([]);
14 | const [status, setStatus] = useState("Getting packs...");
15 |
16 | useEffect(() => {
17 | getSession().then(async (session) => {
18 | const { response, error } = await fetch("/api/twitter/", {
19 | method: "GET",
20 | headers: {
21 | endpoint: `https://api.twitter.com/2/users/${uid}`,
22 | },
23 | }).then((res) => res.json());
24 |
25 | if (!error) {
26 | if (!session || session.user.id !== parseInt(uid)) {
27 | setTitle(`Packs created by @${response.screen_name}`);
28 | getPacks(uid);
29 | }
30 | setUser(response);
31 | }
32 | });
33 | }, []);
34 |
35 | const getPacks = async (id, cache = true) => {
36 | const { response } = await fetchWinstallAPI(
37 | `/packs/${cache ? "users" : "profile"}/${uid}`,
38 | {
39 | headers: {
40 | Authorization: process.env.NEXT_PUBLIC_TWITTER_SECRET,
41 | },
42 | }
43 | );
44 |
45 | if (response) {
46 | setPacks(response);
47 | if (response.length === 0)
48 | setStatus("This user does not have any packs yet.");
49 | if (!cache) localStorage.setItem("ownPacks", JSON.stringify(response));
50 | }
51 | };
52 |
53 | return (
54 |
55 | {user && user.errors ? (
56 |
57 | ) : (
58 |
59 | )}
60 |
61 | {user && user.errors ? (
62 |
63 | ) : (
64 |
65 |
66 |
{title}
67 |
68 | {/*
*/}
69 |
70 |
71 |
72 | {packs.map((pack) => (
73 |
74 |
75 |
76 | ))}
77 |
78 |
79 | {user && packs.length === 0 &&
{status}
}
80 |
81 | )}
82 |
83 | );
84 | }
85 |
86 | UserProfile.getInitialProps = async (ctx) => {
87 | return {
88 | uid: ctx.query.id,
89 | };
90 | };
91 |
92 | export default UserProfile;
93 |
--------------------------------------------------------------------------------
/pages/users/you.js:
--------------------------------------------------------------------------------
1 | import PageWrapper from "../../components/PageWrapper";
2 | import MetaTags from "../../components/MetaTags";
3 | import styles from "../../styles/apps.module.scss";
4 | import PackPreview from "../../components/PackPreview";
5 | import { useEffect, useState } from "react";
6 | import { getSession } from "next-auth/react";
7 | import { useRouter } from "next/router";
8 | import Alert from "../../components/Alert";
9 | import { FiInfo } from "react-icons/fi";
10 | import fetchWinstallAPI from "../../utils/fetchWinstallAPI";
11 |
12 | function OwnProfile() {
13 | const [user, setUser] = useState();
14 | const [packs, setPacks] = useState([]);
15 | const [loading, setLoading] = useState(true);
16 | const [status, setStatus] = useState("Loading...");
17 |
18 | const router = useRouter();
19 |
20 | useEffect(() => {
21 | getSession().then(async (session) => {
22 | if (!session) {
23 | router.push(`/`);
24 | return;
25 | }
26 |
27 | let packs = await localStorage.getItem("ownPacks");
28 |
29 | if (packs != null) {
30 | setPacks(JSON.parse(packs));
31 | setLoading(false);
32 | } else {
33 | getPacks(session.user);
34 | }
35 |
36 | setUser(session.user);
37 | });
38 | }, []);
39 |
40 | const getPacks = async (user) => {
41 | const { response: packs, error } = await fetchWinstallAPI(
42 | `/packs/profile/${user.id}`,
43 | {
44 | headers: {
45 | Authorization: `${user.accessToken},${user.refreshToken}`,
46 | },
47 | }
48 | );
49 |
50 | if (error) {
51 | setStatus(error);
52 | return;
53 | }
54 |
55 | if (packs) {
56 | setPacks(packs);
57 | setLoading(false);
58 | localStorage.setItem("ownPacks", JSON.stringify(packs));
59 | }
60 | };
61 |
62 | const handleDelete = (id) => {
63 | const newPacks = packs.filter((p) => p._id != id);
64 | setPacks(newPacks);
65 | };
66 |
67 | return (
68 |
69 | {user && user.errors ? (
70 |
71 | ) : (
72 |
73 | )}
74 |
75 |
76 |
77 |
Your Packs
78 |
79 | {/*
*/}
80 |
81 |
82 | {loading ? (
83 |
{status}
84 | ) : packs.length === 0 ? (
85 |
86 | You don't have any packs yet. Try creating one first when selecting
87 | apps :)
88 |
89 | ) : (
90 | <>
91 |
95 |
96 |
97 |
98 | {packs.map((pack) => (
99 |
100 |
106 |
107 | ))}
108 |
109 | >
110 | )}
111 |
112 |
113 | );
114 | }
115 |
116 | export default OwnProfile;
117 |
--------------------------------------------------------------------------------
/public/assets/apps/chrome.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omaha-consulting/winstall/f4a902f8762bcc183cb01e7620973e74f9906991/public/assets/apps/chrome.webp
--------------------------------------------------------------------------------
/public/assets/apps/code-insiders.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omaha-consulting/winstall/f4a902f8762bcc183cb01e7620973e74f9906991/public/assets/apps/code-insiders.webp
--------------------------------------------------------------------------------
/public/assets/apps/code.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omaha-consulting/winstall/f4a902f8762bcc183cb01e7620973e74f9906991/public/assets/apps/code.webp
--------------------------------------------------------------------------------
/public/assets/apps/discord.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omaha-consulting/winstall/f4a902f8762bcc183cb01e7620973e74f9906991/public/assets/apps/discord.webp
--------------------------------------------------------------------------------
/public/assets/apps/dropbox.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omaha-consulting/winstall/f4a902f8762bcc183cb01e7620973e74f9906991/public/assets/apps/dropbox.webp
--------------------------------------------------------------------------------
/public/assets/apps/edge.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omaha-consulting/winstall/f4a902f8762bcc183cb01e7620973e74f9906991/public/assets/apps/edge.webp
--------------------------------------------------------------------------------
/public/assets/apps/fallback/chrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omaha-consulting/winstall/f4a902f8762bcc183cb01e7620973e74f9906991/public/assets/apps/fallback/chrome.png
--------------------------------------------------------------------------------
/public/assets/apps/fallback/code-insiders.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omaha-consulting/winstall/f4a902f8762bcc183cb01e7620973e74f9906991/public/assets/apps/fallback/code-insiders.png
--------------------------------------------------------------------------------
/public/assets/apps/fallback/code.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omaha-consulting/winstall/f4a902f8762bcc183cb01e7620973e74f9906991/public/assets/apps/fallback/code.png
--------------------------------------------------------------------------------
/public/assets/apps/fallback/discord.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omaha-consulting/winstall/f4a902f8762bcc183cb01e7620973e74f9906991/public/assets/apps/fallback/discord.png
--------------------------------------------------------------------------------
/public/assets/apps/fallback/dropbox.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omaha-consulting/winstall/f4a902f8762bcc183cb01e7620973e74f9906991/public/assets/apps/fallback/dropbox.png
--------------------------------------------------------------------------------
/public/assets/apps/fallback/edge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omaha-consulting/winstall/f4a902f8762bcc183cb01e7620973e74f9906991/public/assets/apps/fallback/edge.png
--------------------------------------------------------------------------------
/public/assets/apps/fallback/firefox.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omaha-consulting/winstall/f4a902f8762bcc183cb01e7620973e74f9906991/public/assets/apps/fallback/firefox.png
--------------------------------------------------------------------------------
/public/assets/apps/fallback/node.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omaha-consulting/winstall/f4a902f8762bcc183cb01e7620973e74f9906991/public/assets/apps/fallback/node.png
--------------------------------------------------------------------------------
/public/assets/apps/fallback/notion.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omaha-consulting/winstall/f4a902f8762bcc183cb01e7620973e74f9906991/public/assets/apps/fallback/notion.png
--------------------------------------------------------------------------------
/public/assets/apps/fallback/npp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omaha-consulting/winstall/f4a902f8762bcc183cb01e7620973e74f9906991/public/assets/apps/fallback/npp.png
--------------------------------------------------------------------------------
/public/assets/apps/fallback/obs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omaha-consulting/winstall/f4a902f8762bcc183cb01e7620973e74f9906991/public/assets/apps/fallback/obs.png
--------------------------------------------------------------------------------
/public/assets/apps/fallback/python.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omaha-consulting/winstall/f4a902f8762bcc183cb01e7620973e74f9906991/public/assets/apps/fallback/python.png
--------------------------------------------------------------------------------
/public/assets/apps/fallback/sharex.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omaha-consulting/winstall/f4a902f8762bcc183cb01e7620973e74f9906991/public/assets/apps/fallback/sharex.png
--------------------------------------------------------------------------------
/public/assets/apps/fallback/spotify.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omaha-consulting/winstall/f4a902f8762bcc183cb01e7620973e74f9906991/public/assets/apps/fallback/spotify.png
--------------------------------------------------------------------------------
/public/assets/apps/fallback/teams.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omaha-consulting/winstall/f4a902f8762bcc183cb01e7620973e74f9906991/public/assets/apps/fallback/teams.png
--------------------------------------------------------------------------------
/public/assets/apps/fallback/terminal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omaha-consulting/winstall/f4a902f8762bcc183cb01e7620973e74f9906991/public/assets/apps/fallback/terminal.png
--------------------------------------------------------------------------------
/public/assets/apps/fallback/trumpet.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omaha-consulting/winstall/f4a902f8762bcc183cb01e7620973e74f9906991/public/assets/apps/fallback/trumpet.png
--------------------------------------------------------------------------------
/public/assets/apps/fallback/tweeten.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omaha-consulting/winstall/f4a902f8762bcc183cb01e7620973e74f9906991/public/assets/apps/fallback/tweeten.png
--------------------------------------------------------------------------------
/public/assets/apps/fallback/whatsapp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omaha-consulting/winstall/f4a902f8762bcc183cb01e7620973e74f9906991/public/assets/apps/fallback/whatsapp.png
--------------------------------------------------------------------------------
/public/assets/apps/fallback/zoom.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omaha-consulting/winstall/f4a902f8762bcc183cb01e7620973e74f9906991/public/assets/apps/fallback/zoom.png
--------------------------------------------------------------------------------
/public/assets/apps/firefox.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omaha-consulting/winstall/f4a902f8762bcc183cb01e7620973e74f9906991/public/assets/apps/firefox.webp
--------------------------------------------------------------------------------
/public/assets/apps/node.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omaha-consulting/winstall/f4a902f8762bcc183cb01e7620973e74f9906991/public/assets/apps/node.webp
--------------------------------------------------------------------------------
/public/assets/apps/notion.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omaha-consulting/winstall/f4a902f8762bcc183cb01e7620973e74f9906991/public/assets/apps/notion.webp
--------------------------------------------------------------------------------
/public/assets/apps/npp.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omaha-consulting/winstall/f4a902f8762bcc183cb01e7620973e74f9906991/public/assets/apps/npp.webp
--------------------------------------------------------------------------------
/public/assets/apps/obs.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omaha-consulting/winstall/f4a902f8762bcc183cb01e7620973e74f9906991/public/assets/apps/obs.webp
--------------------------------------------------------------------------------
/public/assets/apps/python.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omaha-consulting/winstall/f4a902f8762bcc183cb01e7620973e74f9906991/public/assets/apps/python.webp
--------------------------------------------------------------------------------
/public/assets/apps/sharex.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omaha-consulting/winstall/f4a902f8762bcc183cb01e7620973e74f9906991/public/assets/apps/sharex.webp
--------------------------------------------------------------------------------
/public/assets/apps/spotify.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omaha-consulting/winstall/f4a902f8762bcc183cb01e7620973e74f9906991/public/assets/apps/spotify.webp
--------------------------------------------------------------------------------
/public/assets/apps/teams.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omaha-consulting/winstall/f4a902f8762bcc183cb01e7620973e74f9906991/public/assets/apps/teams.webp
--------------------------------------------------------------------------------
/public/assets/apps/terminal.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omaha-consulting/winstall/f4a902f8762bcc183cb01e7620973e74f9906991/public/assets/apps/terminal.webp
--------------------------------------------------------------------------------
/public/assets/apps/trumpet.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omaha-consulting/winstall/f4a902f8762bcc183cb01e7620973e74f9906991/public/assets/apps/trumpet.webp
--------------------------------------------------------------------------------
/public/assets/apps/tweeten.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omaha-consulting/winstall/f4a902f8762bcc183cb01e7620973e74f9906991/public/assets/apps/tweeten.webp
--------------------------------------------------------------------------------
/public/assets/apps/whatsapp.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omaha-consulting/winstall/f4a902f8762bcc183cb01e7620973e74f9906991/public/assets/apps/whatsapp.webp
--------------------------------------------------------------------------------
/public/assets/apps/zoom.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omaha-consulting/winstall/f4a902f8762bcc183cb01e7620973e74f9906991/public/assets/apps/zoom.webp
--------------------------------------------------------------------------------
/public/assets/clippy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omaha-consulting/winstall/f4a902f8762bcc183cb01e7620973e74f9906991/public/assets/clippy.png
--------------------------------------------------------------------------------
/public/assets/dl.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/public/assets/hero.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omaha-consulting/winstall/f4a902f8762bcc183cb01e7620973e74f9906991/public/assets/hero.png
--------------------------------------------------------------------------------
/public/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/public/assets/winget-pro.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 | site-logo
4 | Created with Sketch.
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/public/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omaha-consulting/winstall/f4a902f8762bcc183cb01e7620973e74f9906991/public/cover.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omaha-consulting/winstall/f4a902f8762bcc183cb01e7620973e74f9906991/public/favicon.ico
--------------------------------------------------------------------------------
/public/generic-app-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omaha-consulting/winstall/f4a902f8762bcc183cb01e7620973e74f9906991/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omaha-consulting/winstall/f4a902f8762bcc183cb01e7620973e74f9906991/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "winstall",
3 | "name": "winstall",
4 | "start_url": "/",
5 | "icons": [
6 | {
7 | "src": "favicon.ico",
8 | "sizes": "64x64 32x32 24x24 16x16",
9 | "type": "image/x-icon"
10 | },
11 | {
12 | "src": "logo192.png",
13 | "type": "image/png",
14 | "sizes": "192x192"
15 | },
16 | {
17 | "src": "logo512.png",
18 | "type": "image/png",
19 | "sizes": "512x512"
20 | }
21 | ],
22 | "display": "standalone",
23 | "theme_color": "#9b2eff",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/public/vercel-dark.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/styles/alert.module.scss:
--------------------------------------------------------------------------------
1 | .alert{
2 | margin: 20px 0;
3 | transition: 0.1s all ease-in-out;
4 | box-shadow: 0px 3px 5px #00000026;
5 | padding: 15px;
6 | border-radius: 10px;
7 | display: flex;
8 | place-items: center;
9 | background-color: var(--card-bg);
10 |
11 | svg{
12 | margin-right: 10px;
13 | }
14 |
15 | p{
16 | margin: 0px;
17 | }
18 | }
19 |
20 | .themed{
21 | background-color: var(--accent);
22 | color: #fff;
23 | }
24 |
25 | .close{
26 | margin-left: auto;
27 | transition: 0.3s all ease-in-out;
28 | cursor: pointer;
29 | margin-right: 0px;
30 |
31 | &:hover{
32 | opacity: 0.75;
33 | }
34 | }
35 |
36 | @media (max-width: 1174px){
37 | .alert{
38 | flex-direction: column;
39 | place-items: flex-start;
40 |
41 | svg{
42 | margin-bottom: 10px;
43 | justify-content: left;
44 | }
45 | }
46 | }
--------------------------------------------------------------------------------
/styles/appList.module.scss:
--------------------------------------------------------------------------------
1 | // app list with images
2 |
3 | .appList{
4 | list-style-type: none;
5 | padding: 0px;
6 | display: grid;
7 | grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
8 | grid-gap: 15px;
9 |
10 | h3{
11 | padding-bottom: 0px;
12 | }
13 | }
14 |
15 | @media(max-width: 1300px){
16 | .popularList{
17 | grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
18 | }
19 | }
20 |
21 | @media(max-width: 500px){
22 | .popularList{
23 | grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
24 | }
25 | }
--------------------------------------------------------------------------------
/styles/apps.module.scss:
--------------------------------------------------------------------------------
1 | .storeList{
2 | list-style-type: none;
3 | padding: 0px;
4 | margin: 0px;
5 | display: grid;
6 | grid-template-columns: repeat(3, 1fr);
7 | grid-gap: 20px;
8 | margin-top: 0px;
9 |
10 | li{
11 | p{
12 | text-align: left !important;
13 | }
14 |
15 |
16 |
17 | }
18 | }
19 |
20 | .all{
21 | grid-template-columns: repeat(4, 1fr);
22 | }
23 |
24 | @media(max-width: 1280px){
25 | .storeList, .all{
26 | grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
27 | }
28 | }
29 |
30 | .notReady{
31 |
32 |
33 |
34 | h3, h4{
35 | margin-top: 0px;
36 | margin-bottom: 5px;
37 | }
38 |
39 | h4{
40 | margin-top: 0px;
41 | margin-bottom: 15px;
42 | opacity: 0.75;
43 | }
44 | }
45 |
46 | .loader{
47 | width: 100%;
48 | height: 100px;
49 | display: flex;
50 | justify-content: center;
51 | margin: 80px 0;
52 | }
53 |
54 | .controls{
55 | display: flex;
56 | justify-content: space-between;
57 | align-items: center;
58 | }
59 |
60 | .btn{
61 | background-color: var(--card-bg);
62 | padding: 10px 20px;
63 | border-radius: 50px;
64 | display: flex;
65 | align-items: center;
66 | height: 50px;
67 | text-decoration: none;
68 | color: var(--textColor);
69 | font-size: 18px;
70 | box-shadow: 0px 3px 5px rgba(0, 0, 0, 0.18);
71 | transition: 0.3s all ease-in-out;
72 | cursor: pointer;
73 | border: 0px;
74 | outline: 0;
75 | margin-left: 20px;
76 | opacity: 0.75;
77 |
78 | svg{
79 | padding-right: 10px;
80 | margin-top: 2px;
81 | font-size: 20px;
82 | }
83 |
84 | &:hover, &:focus{
85 | opacity: 1;
86 | }
87 | }
88 |
89 | @media(max-width: 690px){
90 | .controls{
91 | display: grid;
92 | width: 100% !important;
93 | }
94 |
95 | .btn{
96 | margin-top: 20px;
97 |
98 | margin-left: 0px;
99 | }
100 | }
101 |
102 | .minPagination{
103 | display: grid;
104 | grid-template-columns: 1fr 1fr;
105 | grid-gap: 15px;
106 | }
107 |
108 | .smallBtn {
109 | border-radius: 50%;
110 | width: 50px;
111 | height: 50px;
112 | margin: 0px;
113 | font-size: 30px;
114 | padding: 0px;
115 | display: flex;
116 | justify-content: center;
117 | align-items: center;
118 |
119 | &:disabled{
120 | opacity: 0.60;
121 | cursor: not-allowed;
122 | pointer-events: all;
123 | }
124 |
125 | svg{
126 | padding: 0px;
127 | margin: 0px;
128 | }
129 | }
130 |
131 | .pagination{
132 | display: flex;
133 | align-items: center;
134 | justify-content: center;
135 | padding-top: 50px;
136 | flex-wrap: wrap;
137 |
138 | .pagbtn{
139 | display: grid;
140 | grid-gap: 20px;
141 | grid-template-columns: 1fr 1fr;
142 |
143 |
144 |
145 | button{
146 | display: flex;
147 | align-items: center;
148 | justify-content: center;
149 |
150 | svg{
151 | margin: 0px;
152 | padding: 0px;
153 | }
154 |
155 | &:active, &:focus{
156 | opacity: 1;
157 | }
158 |
159 | &:disabled{
160 | opacity: 0.60;
161 | cursor: not-allowed;
162 | pointer-events: all;
163 | }
164 | }
165 | }
166 |
167 | em{
168 | display: flex;
169 | min-width: 100%;
170 | text-align: center;
171 | padding: 30px 0;
172 | font-style: normal;
173 | align-items: center;
174 | justify-content: center;
175 |
176 | svg{
177 | padding: 0 5px;
178 | }
179 | }
180 | }
181 |
182 | @media(max-width: 710px){
183 | .pagination{
184 | padding: 50px 0 !important;
185 |
186 | em{
187 | display: none;
188 | }
189 | }
190 |
191 | .minPagination{
192 | display: none;
193 | }
194 | }
195 |
196 | .searchLabel label{
197 | display: block;
198 | padding-bottom: 10px;
199 | }
--------------------------------------------------------------------------------
/styles/create.module.scss:
--------------------------------------------------------------------------------
1 | .content{
2 | max-width: 800px;
3 | margin: 0 auto;
4 | padding-bottom: 20px;
5 | text-align: center;
6 | }
7 |
8 | .createForm{
9 | max-width: 500px;
10 | margin: 0 auto;
11 |
12 | .formError{
13 | padding-top: 10px;
14 | display: block;
15 | opacity: 0.75;
16 | font-size: 14px;
17 | }
18 |
19 | label{
20 | display: block;
21 | font-size: 16px;
22 | margin-bottom: 30px;
23 | }
24 |
25 | input[type="text"]{
26 | box-sizing: border-box;
27 | display: block;
28 | margin: 10px 0;
29 | border: 0px;
30 | padding: 0px 25px;
31 | outline: 0px;
32 | height: 50px;
33 | background-color: var(--card-bg);
34 | width: 100%;
35 | border-radius: 50px;
36 | font-size: 15px;
37 | font-family: inherit;
38 | color: var(--textColor);
39 | box-shadow: 0px 3px 5px rgba(0, 0, 0, 0.15);
40 | }
41 |
42 | button{
43 | margin: 0 auto;
44 | background-color: var(--accent);
45 | color: #fff;
46 |
47 | &:disabled{
48 | opacity: 0.5;
49 | cursor: not-allowed;
50 | }
51 | }
52 |
53 | .checkboxContainer{
54 | label {
55 | display: flex;
56 | align-items: center;
57 | align-content: center;
58 | margin-bottom: 0px;
59 |
60 | p{
61 | margin: 0px;
62 | padding-left: 5px;
63 | }
64 | }
65 |
66 | em{
67 | display: block;
68 | font-style: normal;
69 | text-align: left;
70 | margin-left: 30px;
71 | font-size: 14px;
72 | opacity: 0.7;
73 | }
74 |
75 | margin-bottom: 30px;
76 | }
77 | }
78 |
79 | .button{
80 | background-color: var(--card-bg);
81 | padding: 10px 20px;
82 | border-radius: 50px;
83 | margin-top: 15px;
84 | display: inline-block;
85 | text-decoration: none;
86 | color: var(--textColor);
87 | font-size: 18px;
88 | box-shadow: 0px 3px 5px rgba(0, 0, 0, 0.25);
89 | transition: 0.3s all ease-in-out;
90 | cursor: pointer;
91 | border: 0px;
92 | outline: 0;
93 | font-family: 'Nunito', sans-serif;
94 |
95 | div{
96 | display: flex;
97 | place-items: center;
98 | }
99 |
100 | &:hover, &:focus{
101 | opacity: 0.75;
102 | }
103 |
104 | svg{
105 | padding-right: 10px;
106 | margin-top: 2px;
107 | font-size: 20px;
108 | }
109 |
110 |
111 | }
112 |
113 | .accents{
114 | display: grid;
115 | grid-template-columns: repeat(auto-fit, minmax(90px, 1fr));
116 | margin-top: 10px;
117 |
118 | input{
119 | background-color: var(--accent);
120 | appearance: none;
121 | width: 80px !important;
122 | height: 80px !important;
123 | border-radius: 50%;
124 | border: 0px !important;
125 | outline: 0 !important;
126 | margin: 0 10px !important;
127 | cursor: pointer;
128 |
129 | &:checked{
130 | border: 4px solid var(--altAccent);
131 | }
132 | }
133 |
134 | label{
135 | cursor: pointer;
136 | transition: 0.3s all ease-in-out;
137 |
138 | &:hover{
139 | opacity: 0.75;
140 | }
141 | }
142 |
143 | p{
144 | margin: 0px;
145 | padding-top: 5px;
146 | font-size: 15px;
147 | }
148 | }
--------------------------------------------------------------------------------
/styles/donateCard.module.scss:
--------------------------------------------------------------------------------
1 | .container{
2 | transition: 0.1s all ease-in-out;
3 | box-shadow: 0px 3px 5px #00000026;
4 | padding: 25px;
5 | border-radius: 10px;
6 | position: relative;
7 | color: #fff;
8 | background-color: #2575fc;
9 | text-align: left;
10 |
11 | h2 {
12 | margin: 0px;
13 | font-size: 22px;
14 | }
15 |
16 | .buttons {
17 | display: flex;
18 | flex-wrap: wrap;
19 | gap: 10px;
20 |
21 | a {
22 | margin-right: 0px;
23 | }
24 | }
25 |
26 | a {
27 | color: white;
28 | font-weight: bold;
29 | }
30 |
31 | }
32 |
33 | .margin {
34 | margin: 20px 0;
35 | }
36 |
37 | .marginTop {
38 | margin: 40px 0 0;
39 | }
--------------------------------------------------------------------------------
/styles/error.module.scss:
--------------------------------------------------------------------------------
1 | .error{
2 | margin: 50px 0;
3 | text-align: center;
4 |
5 | img{
6 | user-select: none;
7 | }
8 | }
9 |
10 | .btnError{
11 | background-color: var(--card-bg);
12 | padding: 10px 20px;
13 | border-radius: 50px;
14 | margin-top: 15px;
15 | display: inline-block;
16 | text-decoration: none;
17 | color: var(--textColor);
18 | font-size: 18px;
19 | box-shadow: 0px 3px 5px rgba(0, 0, 0, 0.25);
20 | transition: 0.3s all ease-in-out;
21 | cursor: pointer;
22 | border: 0px;
23 | outline: 0;
24 |
25 | div{
26 | display: flex;
27 | }
28 |
29 | &:hover, &:focus{
30 | opacity: 0.75;
31 | }
32 |
33 | svg{
34 | padding-right: 10px;
35 | font-size: 20px;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/styles/exportApps.module.scss:
--------------------------------------------------------------------------------
1 | .generate{
2 | h1{
3 | max-width: 600px;
4 | font-size: 45px;
5 | }
6 |
7 | h3{
8 | font-size: 25px;
9 | opacity: 0.75;
10 | max-width: 600px;
11 | }
12 | p{
13 | font-size: 20px;
14 | max-width: 700px;
15 | }
16 |
17 | textarea{
18 | width: 100%;
19 | padding: 20px;
20 | background-color: var(--card-bg);
21 | border: 0px;
22 | border-radius: 10px;
23 | font-size: 16px;
24 | font-family: monospace;
25 | outline: 0;
26 | resize: none;
27 | display: block;
28 | margin-top: 0px;
29 | margin-right: 0px;
30 | margin-bottom: 10px;
31 | min-height: 120px;
32 | color: var(--textColor);
33 | overflow: hidden;
34 | box-sizing: border-box;
35 | }
36 | }
37 |
38 | .tipContainer{
39 | display: flex;
40 | flex-direction: row;
41 | opacity: 0.75;
42 |
43 | svg{
44 | flex-shrink: 0;
45 | margin-right: 10px;
46 | font-size: 20px;
47 | }
48 |
49 | p{
50 | margin: 0px;
51 | font-size: 17px;
52 | }
53 | }
54 |
55 | .hideSection {
56 | display: none;
57 | }
58 |
59 | .displaySection {
60 | display: block;
61 | }
62 |
63 | .reverse{
64 | flex-direction: row-reverse;
65 | }
66 |
67 | .tabHeader{
68 | list-style-type: none;
69 | padding: 0px;
70 | display: flex;
71 | flex-flow: wrap;
72 | margin-bottom: 0px;
73 |
74 | li{
75 | margin-right: 10px;
76 | border: 3px solid var(--card-bg);
77 | border-radius: 20px;
78 | padding: 5px 15px;
79 | cursor: pointer;
80 | margin-bottom: 10px;
81 | }
82 |
83 | li.active{
84 | border-color: var(--accent);
85 | }
86 | }
87 |
88 | .selectedApps{
89 | padding-bottom: 20px;
90 | }
91 |
92 | @media(max-width: 590px){
93 | .generate{
94 | padding-top: 0px !important;
95 |
96 | textarea{
97 | width: auto !important;
98 | }
99 | }
100 | }
101 |
102 | .expandBlock{
103 | margin-bottom: 10px;
104 |
105 | .center{
106 | display: flex;
107 | align-items: center;
108 |
109 | svg{
110 | margin-right: 10px ;
111 | }
112 | }
113 |
114 | .expandHeader{
115 | display: flex;
116 | align-items: center;
117 | cursor: pointer;
118 | transition: 0.3s all ease-in-out;
119 | user-select: none;
120 | font-size: 15px;
121 | margin-top: 6px;
122 | opacity: 0.65;
123 |
124 | &:hover{
125 | opacity: 0.75;
126 | }
127 |
128 | svg{
129 | margin-right: 5px;
130 | transition: 0.2s all ease-in-out;
131 | }
132 |
133 | svg.expandedIcon{
134 | transform: rotate(180deg);
135 | }
136 | }
137 |
138 | .expandedHeader{
139 | opacity: 1;
140 | }
141 |
142 | label{
143 | display: flex;
144 | align-items: center;
145 | margin-bottom: 5px;
146 | cursor: pointer;
147 |
148 | input{
149 | margin: 0px;
150 | margin-right: 10px;
151 | }
152 |
153 | p{
154 | margin: 0px;
155 | }
156 |
157 | code {
158 | background-color: var(--card-bg);
159 | padding: 5px;
160 | color: var(--error);
161 | border-radius: 5px;
162 | }
163 | }
164 |
165 | label.radioContainer{
166 | display: flex;
167 | flex-direction: column;
168 | align-items: flex-start;
169 | margin-top: 10px;
170 |
171 | div {
172 | display: flex;
173 | flex-direction: row;
174 | justify-content: flex-start;
175 | align-items: flex-start;
176 | margin-top: 5px;
177 |
178 | label{
179 | margin-right: 10px;
180 | }
181 | }
182 | }
183 |
184 | label.text{
185 | flex-direction: column;
186 | justify-content: flex-start;
187 | align-items: flex-start;
188 |
189 | p{
190 | margin: 0px;
191 | }
192 |
193 | input{
194 | margin-top: 10px;
195 | width: 100%;
196 | padding: 10px 15px;
197 | background-color: var(--card-bg);
198 | border: 0px;
199 | border-radius: 10px;
200 | font-size: 15px;
201 | font-family: monospace;
202 | outline: 0;
203 | margin-bottom: 10px;
204 | color: var(--textColor);
205 | box-sizing: border-box;
206 | }
207 | }
208 | }
209 |
210 | .scriptHeader{
211 | display: flex;
212 | justify-content: space-between;
213 | place-items: center;
214 |
215 | h3{
216 | font-size: 1.5em;
217 | margin: 0px;
218 | padding: 0px;
219 | }
220 | }
221 |
222 |
223 | @media(max-width: 620px){
224 | .scriptHeader{
225 | display: block;
226 | text-align: left;
227 |
228 | h3{
229 | margin-bottom: 5px;
230 | }
231 | }
232 |
233 | .box{
234 | display: grid;
235 | }
236 | }
--------------------------------------------------------------------------------
/styles/featurePromoter.module.scss:
--------------------------------------------------------------------------------
1 | .container{
2 | min-height: 300px;
3 | transition: 0.1s all ease-in-out;
4 | box-shadow: 0px 3px 5px #00000026;
5 | padding: 20px 50px;
6 | border-radius: 10px;
7 | position: relative;
8 |
9 | img{
10 | position: absolute;
11 | right: 0;
12 | top: 0;
13 | pointer-events: none;
14 | opacity: 0.75;
15 | }
16 |
17 | .slogan{
18 | position: absolute;
19 | bottom: 50px;
20 | color: #fff;
21 |
22 | h3{
23 | margin: 0px;
24 | }
25 |
26 | h1{
27 | margin: 0px;
28 | max-width: 600px;
29 | }
30 | }
31 |
32 | .hide{
33 | position: absolute;
34 | top: 20px;
35 | right: 20px;
36 | border-radius: 50px;
37 | border: 0px;
38 | outline: 0px;
39 | width: 40px;
40 | height: 40px;
41 | font-size: 20px;
42 | display: flex;
43 | justify-content: center;
44 | align-items: center;
45 | transition: 0.1s all ease-in-out;
46 | cursor: pointer;
47 |
48 | &:hover{
49 | opacity: 0.75;
50 | }
51 | }
52 | }
53 |
54 | @media(max-width: 1300px){
55 | .container{
56 | img{
57 | max-width: 500px;
58 | }
59 | }
60 | }
61 |
62 | @media(max-width: 995px){
63 | .container{
64 | img{
65 | max-width: 450px;
66 | }
67 | }
68 | }
69 |
70 | @media(max-width: 900px){
71 | .container{
72 | padding: 15px 30px;
73 |
74 | img{
75 | display: none;
76 |
77 | }
78 | }
79 | }
80 |
81 | @media(max-width: 750px){
82 | .container{
83 | min-height: auto;
84 | height: auto;
85 |
86 | .slogan{
87 | position: initial;
88 | padding: 30px 0px;
89 | }
90 |
91 | h1{
92 | font-size: 22px;
93 | }
94 | }
95 | }
96 |
97 |
--------------------------------------------------------------------------------
/styles/footer.module.scss:
--------------------------------------------------------------------------------
1 | .footer {
2 | margin-top: 30px;
3 | font-size: 20px;
4 | display: flex;
5 | padding: 20px 0;
6 | justify-content: space-between;
7 | align-content: center;
8 | align-tracks: center;
9 |
10 | .brand {
11 | display: flex;
12 | justify-content: flex-start;
13 | align-items: flex-start;
14 |
15 | p {
16 | margin: 0px;
17 | font-size: 17px;
18 | margin-top: 2px;
19 | opacity: 0.75;
20 | }
21 |
22 | em {
23 | margin: 0px;
24 | font-size: 14px;
25 | opacity: 0.75;
26 | font-style: normal;
27 | }
28 | }
29 |
30 | a {
31 | color: var(--textColor);
32 | text-decoration: none;
33 | transition: 0.3s all ease-in-out;
34 |
35 | &:hover {
36 | color: var(--accent);
37 | }
38 | }
39 |
40 | img {
41 | width: 30px;
42 | transition: 0.3s all ease-in-out;
43 | padding-top: 5px;
44 | cursor: pointer;
45 | margin-right: 10px;
46 |
47 | &:hover {
48 | opacity: 0.75;
49 | }
50 | }
51 |
52 | .heart {
53 | color: #af0d16;
54 | }
55 |
56 | ul {
57 | display: flex;
58 | padding: 0px;
59 | margin: 0px;
60 | list-style-type: none;
61 | font-size: 17px;
62 | align-items: center;
63 |
64 | li {
65 | margin-left: 20px;
66 |
67 | a {
68 | opacity: 0.75;
69 |
70 | &:hover {
71 | opacity: 1;
72 | }
73 | }
74 | }
75 | }
76 | }
77 |
78 |
79 | .backToTop {
80 | display: none;
81 | position: fixed;
82 | bottom: 10px;
83 | right: 20px;
84 | z-index: 99;
85 | border: none;
86 | outline: none;
87 | background-color: var(--card-bg);
88 | color: var(--textColor);
89 | cursor: pointer;
90 | border-radius: 50%;
91 | transition: 0.3s all ease-in-out;
92 | box-shadow: 0px 3px 5px rgba(0, 0, 0, 0.25);
93 | opacity: 0.75;
94 | text-align: center;
95 | width: 50px;
96 | height: 50px;
97 | justify-content: center;
98 | font-size: 22px;
99 | animation: fadeIn ease 1s;
100 | -webkit-animation: fadeIn ease 1s;
101 | -moz-animation: fadeIn ease 1s;
102 | -o-animation: fadeIn ease 1s;
103 | -ms-animation: fadeIn ease 1s;
104 |
105 | svg {
106 | padding-top: 15px;
107 | }
108 |
109 | &:hover,
110 | &:focus {
111 | opacity: 1;
112 | color: var(--textColor) !important;
113 | }
114 | }
115 |
116 | @keyframes fadeIn {
117 | 0% {
118 | opacity: 0;
119 | }
120 |
121 | 100% {
122 | opacity: 0.75;
123 | }
124 | }
125 |
126 | @-moz-keyframes fadeIn {
127 | 0% {
128 | opacity: 0;
129 | }
130 |
131 | 100% {
132 | opacity: 0.75;
133 | }
134 | }
135 |
136 | @-webkit-keyframes fadeIn {
137 | 0% {
138 | opacity: 0;
139 | }
140 |
141 | 100% {
142 | opacity: 0.75;
143 | }
144 | }
145 |
146 | @-o-keyframes fadeIn {
147 | 0% {
148 | opacity: 0;
149 | }
150 |
151 | 100% {
152 | opacity: 0.75;
153 | }
154 | }
155 |
156 | @-ms-keyframes fadeIn {
157 | 0% {
158 | opacity: 0;
159 | }
160 |
161 | 100% {
162 | opacity: 0.75;
163 | }
164 | }
165 |
166 | @media(max-width: 1490px) and (min-width: 791px) {
167 | .selectionOpen.backToTop {
168 | bottom: 90px;
169 | }
170 | }
171 |
172 |
173 | @media(max-width: 490px) {
174 | .footer {
175 | flex-direction: column;
176 | justify-content: center;
177 |
178 | .brand {
179 | margin: 0 auto;
180 | margin-bottom: 10px;
181 | }
182 |
183 | ul {
184 | justify-content: center;
185 | font-size: 15px;
186 | display: grid;
187 | grid-auto-flow: column;
188 | grid-gap: 10px;
189 |
190 | li {
191 | padding: 0px;
192 | margin: 0px;
193 | }
194 | }
195 | }
196 | }
--------------------------------------------------------------------------------
/styles/home.module.scss:
--------------------------------------------------------------------------------
1 |
2 |
3 | .intro {
4 | padding-top: 50px;
5 | margin-bottom: 50px;
6 |
7 | h1{
8 | max-width: 600px;
9 | font-size: 45px;
10 | }
11 | }
12 |
13 | p.lead {
14 | font-size: 150%;
15 | }
16 |
17 | .selectedApps{
18 | padding-bottom: 20px;
19 | margin-top: 50px;
20 | }
21 |
22 | @media(max-width: 590px){
23 | .intro{
24 | padding-top: 0px !important;
25 |
26 | textarea{
27 | width: auto !important;
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/styles/listSort.module.scss:
--------------------------------------------------------------------------------
1 | .sort{
2 | display: flex;
3 | align-items: center;
4 | justify-content: center;
5 | position: relative;
6 |
7 | label{
8 | padding-right: 10px;
9 | }
10 |
11 | select{
12 | height: 30px;
13 | padding: 0 15px;
14 | padding-right: 30px;
15 | line-height: 32px;
16 | border: 0px;
17 | outline: 0;
18 | background-color: var(--card-bg);
19 | border-radius: 50px;
20 | cursor: pointer;
21 | color: var(--textColor);
22 | box-shadow: 0px 3px 5px #00000026;
23 | transition: 0.3s all ease-in-out;
24 | -webkit-appearance: none;
25 | font-family: 'Nunito', sans-serif;
26 |
27 | &:hover{
28 | opacity: 0.75;
29 | }
30 | }
31 |
32 | svg{
33 | position: absolute;
34 | right: 10px;
35 | pointer-events: none;
36 | }
37 | }
38 |
39 | @media(max-width: 690px){
40 | .sort{
41 | margin-bottom: 20px;
42 | }
43 |
44 | }
--------------------------------------------------------------------------------
/styles/nav.module.scss:
--------------------------------------------------------------------------------
1 | .nav{
2 | display: flex;
3 |
4 | .ddOnly{
5 | display: none;
6 | }
7 |
8 | span, a{
9 | background-color: var(--card-bg);
10 | cursor: pointer;
11 | user-select: none;
12 | transition: 0.3s all ease-in-out;
13 | display: flex;
14 | align-self: center;
15 | justify-content: center;
16 | border: 0px !important;
17 |
18 | box-shadow: 0px 3px 5px rgba(0, 0, 0, 0.25);
19 |
20 | &:hover{
21 | opacity: 0.75;
22 | }
23 | }
24 |
25 | .justIcon{
26 | padding: 0px;
27 | width: 50px;
28 |
29 | svg{
30 | padding-right: 0px;
31 | }
32 | }
33 |
34 | .user{
35 | padding: 0px;
36 |
37 | img{
38 | width: 50px;
39 | height: 50px;
40 | margin: 0px;
41 | }
42 | }
43 |
44 | a{
45 | font-size: 20px;
46 | height: 50px;
47 | line-height: 50px;
48 | padding: 0 20px;
49 | border-radius: 50px;
50 | margin-right: 15px;
51 | font-weight: normal;
52 | color: var(--textColor);
53 | display: flex;
54 | align-items: center;
55 |
56 | p{
57 | margin: 0px;
58 | padding: 0px;
59 | }
60 |
61 | img{
62 | width: 25px;
63 | height: 25px;
64 | border-radius: 50px;
65 | margin-right: 10px;
66 | }
67 |
68 | svg{
69 | font-size: 18px;
70 | padding-right: 10px;
71 | }
72 |
73 | &:hover{
74 | color: var(--textColor) !important;
75 | }
76 | }
77 |
78 |
79 | span{
80 | width: 50px;
81 | height: 50px;
82 | border-radius: 50%;
83 | font-size: 22px;
84 | margin-right: 15px;
85 |
86 | svg{
87 | padding-top: 15px;
88 | }
89 | }
90 | }
91 |
92 | .dropdown{
93 | display: none;
94 | background-color: var(--card-bg);
95 | cursor: pointer;
96 | user-select: none;
97 | transition: 0.3s all ease-in-out;
98 | align-self: center;
99 | justify-content: center;
100 | place-items: center;
101 | border: 0px !important;
102 | width: 50px;
103 | height: 50px;
104 | border-radius: 50%;
105 | font-size: 22px;
106 | box-shadow: 0px 3px 5px rgba(0, 0, 0, 0.25);
107 | color: inherit;
108 |
109 | &:hover{
110 | opacity: 0.75;
111 | }
112 | }
113 |
114 |
115 | @media(max-width: 590px){
116 | .nav{
117 | right: 30px !important;
118 | }
119 | }
120 |
121 | @media(max-width: 900px){
122 |
123 | .dropdown{
124 | display: flex;
125 | }
126 |
127 | .nav{
128 | display: none;
129 | position: absolute;
130 | right: 55px;
131 | top: 80px;
132 | z-index: 999;
133 | width: 200px;
134 | background-color: var(--card-bg);
135 | box-shadow: 0px 3px 5px rgba(0, 0, 0, 0.25);
136 | border-radius: 10px;
137 | text-align: left;
138 | padding: 0 20px;
139 |
140 | a, span{
141 | background-color: transparent;
142 | box-shadow: none;
143 | border-radius: 0px;
144 | width: 100%;
145 | margin: 0px;
146 | padding: 0px;
147 | justify-content: flex-start;
148 | }
149 |
150 | .ddOnly{
151 | display: block;
152 | font-size: 20px;
153 | height: 50px;
154 | margin: 0px;
155 | line-height: 50px;
156 | }
157 |
158 | .justIcon{
159 | width: auto;
160 |
161 | svg{
162 | padding-right: 10px;
163 | }
164 | }
165 |
166 | .user img{
167 | width: 23px;
168 | height: 23px;
169 | margin-right: 10px;
170 | }
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/styles/packPage.module.scss:
--------------------------------------------------------------------------------
1 | .content{
2 | max-width: 800px;
3 | margin: 0 auto;
4 | text-align: center;
5 |
6 | .time{
7 | svg{
8 | vertical-align: middle;
9 | }
10 | }
11 |
12 | .author{
13 | display: flex;
14 | place-items: center;
15 | justify-content: center;
16 | margin: 5px 0;
17 | text-decoration: none;
18 | color: inherit;
19 | transition: 0.3s all ease-in-out;
20 |
21 | &:hover{
22 | opacity: 0.75;
23 | }
24 |
25 | img{
26 | width: 30px;
27 | height: 30px;
28 | border-radius: 50%;
29 | margin-right: 10px;
30 | }
31 | }
32 |
33 | .packGet{
34 | display: grid;
35 | margin-bottom: 16px;
36 | justify-content: center;
37 | grid-gap: 10px;
38 | grid-auto-flow: column;
39 |
40 |
41 | }
42 | }
43 |
44 | .getScript{
45 | background-color: var(--main-bg);
46 | margin: 20px 0;
47 | padding: 20px;
48 | border-radius: 10px;
49 | box-shadow: 0px 3px 5px #00000026;
50 | border: 5px solid var(--accent);
51 |
52 | textarea{
53 | width: 100%;
54 | box-sizing: border-box;
55 | padding: 20px;
56 | background-color: var(--card-bg);
57 | border: 0px;
58 | border-radius: 10px;
59 | font-size: 18px;
60 | font-family: monospace;
61 | outline: 0;
62 | resize: none;
63 | display: block;
64 | margin-bottom: 10px;
65 | min-height: 150px;
66 | color: var(--textColor);
67 | overflow: hidden;
68 | margin-top: 10px;
69 | }
70 |
71 | p{
72 | text-align: left;
73 | }
74 |
75 | }
76 |
77 |
78 | .scriptHeader{
79 | display: flex;
80 | justify-content: space-between;
81 | place-items: center;
82 |
83 | h3{
84 | font-size: 1.5em;
85 | margin: 0px;
86 | padding: 0px;
87 | }
88 |
89 |
90 | }
91 |
92 |
93 | @media(max-width: 620px){
94 | .scriptHeader{
95 | display: block;
96 | text-align: left;
97 |
98 | h3{
99 | margin-bottom: 5px;
100 | }
101 | }
102 |
103 | .box{
104 | display: grid;
105 | }
106 | }
107 |
108 | @media(max-width: 530px){
109 | .packGet{
110 | grid-gap: 15px !important;
111 |
112 | a{
113 | padding: 0px;
114 | box-shadow: none;
115 | background: transparent;
116 | background-image: none !important;
117 | color: var(--textColor);
118 | margin: 20px 0;
119 | font-size: 16px;
120 |
121 | svg{
122 | padding-right: 5px;
123 | }
124 | }
125 | }
126 | }
127 |
128 |
--------------------------------------------------------------------------------
/styles/packPreview.module.scss:
--------------------------------------------------------------------------------
1 | .packCard{
2 | background-color: var(--card-bg);
3 | border-radius: 10px;
4 | overflow: hidden;
5 | transition: 0.1s all ease-in-out;
6 | box-shadow: 0px 3px 5px #00000026;
7 | display: flex;
8 | flex-direction: column;
9 | justify-content: flex-start;
10 | color: var(--textColor);
11 | text-decoration: none;
12 | height: 100%;
13 |
14 |
15 |
16 | .singleIcon ul{
17 | display: flex !important;
18 | }
19 |
20 | .packIcons{
21 | padding: 0px;
22 | justify-content: center;
23 | background-image: linear-gradient(160deg,#9708CC 0%,#0078D7 100%);
24 | padding: 20px;
25 | min-height: 100px;
26 | border-radius: 10px 10px 0px 0px;
27 |
28 | ul{
29 | list-style-type: none;
30 | padding: 0px;
31 | display: grid;
32 | grid-gap: 10px;
33 | grid-auto-flow: row;
34 | grid-template-columns: repeat(4, 1fr);
35 |
36 | li{
37 | background-color: #fff;
38 | width: 50px;
39 | height: 50px;
40 | border-radius: 50%;
41 | display: grid;
42 | place-items: center;
43 | box-shadow: 0px 3px 5px #00000026;
44 |
45 |
46 | img, picture{
47 | width: 25px;
48 | height: 25px;
49 | margin: 0 auto;
50 | }
51 | }
52 | }
53 | }
54 |
55 | .packMeta{
56 | padding: 20px;
57 | transition: 0.1s all ease-in-out;
58 | flex-grow: 1;
59 | display: flex;
60 | flex-direction: column;
61 |
62 | h3{
63 | margin: 0px;
64 | color: var(--textColor);
65 | }
66 |
67 | a{
68 | text-decoration: none;
69 | }
70 |
71 | p{
72 | margin: 5px 0;
73 | }
74 |
75 | .includes{
76 | opacity: 0.75;
77 | font-size: 16px;
78 | }
79 |
80 | a{
81 | display: inline-block;
82 | }
83 | }
84 | }
85 |
86 |
87 | .delete{
88 | &:hover{
89 | opacity: 1 !important;
90 | }
91 | }
92 |
93 | .packEdit{
94 | display: flex;
95 | vertical-align: middle;
96 |
97 | }
--------------------------------------------------------------------------------
/styles/packsAppList.module.scss:
--------------------------------------------------------------------------------
1 | .appsList{
2 | list-style-type: none;
3 | padding: 0px;
4 | margin: 0px;
5 |
6 | div.appCard{
7 | margin-bottom: 20px;
8 | background-color: var(--card-bg);
9 | padding: 20px;
10 | border-radius: 10px;
11 | box-shadow: 0px 3px 5px #00000026;
12 | position: relative;
13 | }
14 |
15 | li{
16 | border: 0px !important;
17 | overflow: visible;
18 |
19 | div{
20 | margin-bottom: 0px;
21 | }
22 | }
23 |
24 | .unselectApp{
25 | position: absolute;
26 | right: 15px;
27 | top: 15px;
28 | background-color: var(--main-bg) !important;
29 | border-radius: 50px;
30 | color: var(--textColor);
31 | box-shadow: 0px 3px 5px rgba(0, 0, 0, 0.18);
32 | transition: 0.3s all ease-in-out;
33 | cursor: pointer;
34 | border: 0px;
35 | width: 35px !important;
36 | height: 35px !important;
37 | outline: 0;
38 | display: flex;
39 | align-items: center;
40 | justify-content: center;
41 |
42 | padding: 0px !important;
43 |
44 | svg{
45 | padding-right: 0px !important;
46 | margin-top: 0px !important;
47 | width: 20px;
48 | height: 20px;
49 | color: var(--textColor);
50 | transition: 0.1s all ease-in-out;
51 | transform: rotate(45deg);
52 | }
53 |
54 | &:active, &:focus{
55 | opacity: 0.7 !important;
56 | }
57 |
58 | &:hover{
59 | opacity: 0.75;
60 | }
61 | }
62 | }
63 |
64 | .noDragList{
65 | display: grid;
66 | // grid-template-columns: repeat( auto-fit, minmax(350px, 1fr));
67 | grid-gap: 16px;
68 |
69 | div.appCard{
70 | margin-bottom: 0px;
71 | }
72 | }
73 |
74 | .addModal{
75 | position: absolute;
76 | outline: none;
77 | padding: 20px;
78 | border-radius: 10px;
79 | background-color: var(--main-bg);
80 | border: 3px solid var(--accent);
81 | top: 50%;
82 | left: 50%;
83 | min-width: 700px;
84 | min-height: 450px;
85 | transform: translate(-50%, -50%);
86 | box-shadow: 0px 3px 15px rgba(0, 0, 0, 0.18);
87 |
88 | .modalHeader{
89 | display: flex;
90 | justify-content: space-between;
91 | place-items: center;
92 | margin-bottom: 10px;
93 |
94 | h2{
95 | margin: 0px;
96 | }
97 |
98 | svg{
99 | vertical-align: middle;
100 | font-size: 25px;
101 | transition: 0.3s all ease-in-out;
102 | cursor: pointer;
103 |
104 | &:hover{
105 | opacity: 0.75;
106 | }
107 | }
108 | }
109 |
110 | }
111 |
112 | .modalOverlay{
113 | position: fixed;
114 | top: 0px;
115 | left: 0px;
116 | right: 0px;
117 | bottom: 0px;
118 | background-color: rgba(0, 0, 0, 0.35);
119 | }
120 |
121 | @media(max-width: 790px){
122 | .addModal{
123 | width: 100%;
124 | height: 100%;
125 | top: 0;
126 | left: 0;
127 | border: 0px;
128 | transform: none;
129 | box-sizing: border-box;
130 | border-radius: 0px;
131 | }
132 | }
--------------------------------------------------------------------------------
/styles/prettyApp.module.scss:
--------------------------------------------------------------------------------
1 | .app{
2 | background-color: var(--card-bg);
3 | padding: 20px;
4 | border: 5px solid var(--card-bg);
5 | border-radius: 10px;
6 | cursor: pointer;
7 | overflow: hidden;
8 | transition: 0.1s all ease-in-out;
9 | box-shadow: 0px 3px 5px rgba(0, 0, 0, 0.15);
10 |
11 | .imgContainer{
12 | min-height: 80px;
13 | position: relative;
14 |
15 | img{
16 | width: 80px;
17 | height: auto;
18 | margin: 0 auto;
19 | display: block;
20 | user-select: none;
21 |
22 | position: absolute;
23 | top: 50%;
24 | left: 50%;
25 | transform: translate(-50%, -50%);
26 | }
27 | }
28 |
29 | &:hover{
30 | opacity: 0.75;
31 | }
32 |
33 | h3{
34 | margin-top: 0px;
35 | margin-bottom: 5px;
36 |
37 | img{
38 | user-select: none;
39 | width: 25px;
40 | height: 25px;
41 | display: inline-block;
42 | padding-right: 5px;
43 | }
44 | }
45 |
46 | .moreInfo{
47 | display: flex;
48 | align-items: center;
49 | justify-content: center;
50 |
51 | span{
52 | opacity: 0.75;
53 | font-size: 15px;
54 | transition: 0.3s all ease-in-out;
55 | border-bottom: 3px solid transparent;
56 | display: flex;
57 | align-items: center;
58 | justify-content: center;
59 | padding-bottom: 3px;
60 | padding-top: 5px;
61 |
62 | svg{
63 | transition: 0.3s all ease-in-out;
64 | }
65 |
66 | &:hover{
67 | svg{
68 | padding-left: 5px;
69 | }
70 | }
71 | }
72 | }
73 | }
74 |
75 |
76 | .selected{
77 | border: 5px solid var(--accent);
78 | }
79 |
80 | .imgHeader{
81 | text-align: center;
82 | padding: 20px;
83 | margin: 0px;
84 | }
85 |
86 | @media(max-width: 690px){
87 | .imgContainer{
88 | min-height: 50px !important;
89 |
90 | img{
91 | width: 50px !important
92 | }
93 | }
94 |
95 | }
--------------------------------------------------------------------------------
/styles/recommendations.module.scss:
--------------------------------------------------------------------------------
1 | .recommendations{
2 | margin: 16px 0;
3 | display: grid;
4 | grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
5 | grid-gap: 15px;
6 | }
7 |
8 | @media(max-width: 450px){
9 | .recommendations{
10 | grid-template-columns: 1fr;
11 | }
12 | }
13 |
14 | @keyframes bounce {
15 | 0%, 100% {
16 | transform: translateY(0);
17 | }
18 | 50% {
19 | transform: translateY(-5px);
20 | }
21 | }
22 |
23 | .packHeader{
24 | display: block;
25 | padding: 10px 20px;
26 | border-radius: 10px 10px 0px 0px;
27 | color: #fff;
28 | padding-top: 30px;
29 | text-decoration: none;
30 | cursor: pointer;
31 |
32 | &:hover{
33 | opacity: 0.85;
34 |
35 | svg{
36 | animation: bounce 0.8s ease-in-out infinite forwards ;
37 | }
38 | }
39 |
40 | svg{
41 | font-size: 20px;
42 | transition: 0.3s all ease-in-out;
43 | }
44 |
45 | h3{
46 | margin: 0px;
47 | padding-bottom: 5px;
48 | }
49 |
50 | p{
51 | margin: 0px;
52 | opacity: 0.85;
53 | }
54 | }
55 |
56 | .recommendation{
57 | background-color: var(--card-bg);
58 | border-radius: 10px;
59 | overflow: hidden;
60 | transition: 0.1s all ease-in-out;
61 | box-shadow: 0px 3px 5px #00000026;
62 | }
63 |
64 | .packListContainer{
65 | padding: 20px;
66 |
67 |
68 | h3{
69 | border-bottom: 3px solid var(--accent);
70 | padding: 0px;
71 | margin: 0px;
72 | margin-bottom: 20px;
73 | }
74 |
75 | }
76 |
77 | .appContainer{
78 | display: flex;
79 | justify-content: flex-start;
80 | vertical-align: middle;
81 | margin-bottom: 10px;
82 |
83 | a{
84 | display: flex;
85 | color: inherit;
86 | text-decoration: none;
87 | transition: 0.3s all ease-in-out;
88 |
89 | &:hover{
90 | opacity: 0.75;
91 | }
92 | }
93 |
94 | img{
95 | width: 25px;
96 | height: 25px;
97 | margin-right: 10px;
98 | }
99 |
100 | h4{
101 | margin: 0px;
102 | line-height: 30px;
103 | }
104 |
105 | .selectApp {
106 | text-decoration: none;
107 | color: var(--textColor);
108 | transition: 0.3s all ease-in-out;
109 | cursor: pointer;
110 | border: 0px;
111 | background-color: transparent;
112 | outline: 0;
113 | display: flex;
114 | align-items: center;
115 | justify-content: center;
116 | margin-left: auto;
117 | flex-shrink: 0;
118 | padding: 0px !important;
119 |
120 | svg{
121 | padding-right: 0px !important;
122 | margin-top: 0px !important;
123 | width: 20px;
124 | height: 20px;
125 | color: var(--textColor);
126 | transition: 0.1s all ease-in-out;
127 | }
128 |
129 | &:active, &:focus{
130 | opacity: 0.7 !important;
131 | }
132 |
133 | &:hover{
134 | opacity: 0.75;
135 | }
136 | }
137 | }
138 |
139 | .selected{
140 | .selectApp{
141 | svg{
142 | transform: rotate(45deg);
143 | color: var(--accent);
144 | }
145 |
146 | &:active, &:focus{
147 | opacity: 1 !important;
148 | }
149 | }
150 | }
151 |
152 | .viewPack{
153 | svg{
154 | transition: 0.3s all ease-in-out;
155 | }
156 |
157 | &:hover{
158 | svg{
159 | padding-left: 5px;
160 | }
161 | }
162 | }
163 |
164 |
--------------------------------------------------------------------------------
/styles/search.module.scss:
--------------------------------------------------------------------------------
1 | .searchLabel{
2 | display: block;
3 | padding-bottom: 10px;
4 | }
5 |
6 | .searchBox{
7 | box-shadow: 0px 3px 5px rgba(0, 0, 0, 0.25);
8 | border-radius: 50px;
9 | height: 50px;
10 | background-color: var(--card-bg);
11 | display: inline-block;
12 | line-height: 55px;
13 | position: relative;
14 |
15 | .searchInner{
16 | display: flex;
17 | justify-content: flex-start;
18 | align-items: center;
19 |
20 |
21 | }
22 |
23 | .tip {
24 | position: absolute;
25 | right: -40px;
26 | top: 0px;
27 |
28 | a{
29 | display: inline-block;
30 | color: inherit;
31 | margin-top: 2px;
32 | }
33 |
34 | &:focus-within{
35 | .tipData{
36 | opacity: 1;
37 | pointer-events: auto;
38 | }
39 | }
40 |
41 | svg{
42 | width: 20px;
43 | height: 20px;
44 | padding-left: 0px !important;
45 | cursor: pointer;
46 | transition: 0.3s all ease-in-out;
47 |
48 | &:hover{
49 | opacity: 0.65;
50 | }
51 |
52 | }
53 |
54 | .tipData{
55 | opacity: 0;
56 | pointer-events: none;
57 | background-color: var(--accent);
58 | color: #fff;
59 | box-shadow: 0px 3px 25px rgba(0, 0, 0, 0.15);
60 | padding: 20px;
61 | border-radius: 10px;
62 | position: absolute;
63 | top: 50px;
64 | z-index: 9;
65 | min-width: 320px;
66 | transition: 0.3s all ease-in-out;
67 |
68 | p{
69 | margin: 0px;
70 | margin-bottom: 10px;
71 | line-height: 18px;
72 | }
73 |
74 | ul{
75 | padding: 0px;
76 | list-style-type: none;
77 | margin: 0px;
78 |
79 | li{
80 | margin: 0px;
81 | padding: 0px;
82 | line-height: 20px;
83 | margin-bottom: 5px;
84 |
85 | code{
86 | background-color: var(--card-bg);
87 | color: var(--textColor);
88 | padding: 2px 5px;
89 | border-radius: 2px;
90 | }
91 | }
92 | }
93 | }
94 | }
95 |
96 | svg{
97 | padding-left: 15px;
98 | }
99 |
100 | input[type="text"]{
101 | border: 0px;
102 | outline: 0px;
103 | background-color: transparent;
104 | height: 50px;
105 | padding: 0 15px;
106 | font-size: 17px;
107 | min-width: 210px;
108 | color: var(--textColor);
109 | }
110 |
111 |
112 | .searchHint{
113 | margin-top: 0px;
114 | }
115 |
116 | }
117 |
118 | @media(max-width: 495px){
119 | .searchBox{
120 | width: 100%;
121 |
122 | input[type=text]{
123 | width: calc(100% - 65px) !important;
124 | }
125 | }
126 |
127 | }
128 |
129 | .noresults{
130 | position: absolute;
131 | }
132 |
133 | @media(max-width: 790px){
134 | .tip{
135 | display: none;
136 | }
137 | }
--------------------------------------------------------------------------------
/utils/fetchWinstallAPI.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Helper method for requesting resources from the winstall-api.
3 | * @param {*} path - path of the resource
4 | * @param {*} givenOptions - any additional header options
5 | * @param {*} throwErr - flag to indicate whether an error should be thrown
6 | * @returns
7 | */
8 | const fetchWinstallAPI = async (path, givenOptions, throwErr) => {
9 | const url = `${process.env.NEXT_PUBLIC_WINGET_API_BASE}${path}`;
10 |
11 | let additionalOptions = { ...givenOptions };
12 | let headerOptions;
13 |
14 | if (additionalOptions) {
15 | headerOptions = { ...givenOptions?.headers };
16 | delete additionalOptions["headers"];
17 | }
18 |
19 | let response, error;
20 |
21 | try {
22 | const res = await fetch(url, {
23 | headers: {
24 | AuthKey: process.env.NEXT_PUBLIC_WINGET_API_KEY,
25 | AuthSecret: process.env.NEXT_PUBLIC_WINGET_API_SECRET,
26 | ...headerOptions,
27 | },
28 | ...additionalOptions,
29 | redirect: "follow",
30 | });
31 |
32 |
33 | if (!res.ok) {
34 | // if `throwErr` is true we fail deployments
35 | if (throwErr) {
36 | throw new Error((await res.json()).error);
37 | }
38 |
39 | error = (await res.json()).error;
40 | } else {
41 | response = await res.json();
42 | }
43 | } catch (err) {
44 | error = err.message;
45 |
46 | // if `throwErr` is true we fail deployments
47 | if (throwErr) {
48 | throw new Error(err);
49 | }
50 | }
51 |
52 | return { response, error };
53 | };
54 |
55 | module.exports = fetchWinstallAPI;
56 |
--------------------------------------------------------------------------------
/utils/generateWingetImport.js:
--------------------------------------------------------------------------------
1 | // schema from https://raw.githubusercontent.com/microsoft/winget-cli/master/schemas/JSON/packages/packages.schema.1.0.json
2 |
3 | let wingetImportSchema = {
4 | "$schema": "https://aka.ms/winget-packages.schema.1.0.json",
5 | "WinGetVersion": "0.3.11201",
6 | "Sources": [
7 | {
8 | "Packages": [
9 | // we just need to populate this
10 | ],
11 | "SourceDetails": {
12 | "Argument" : "https://winget.azureedge.net/cache",
13 | "Identifier" : "Microsoft.Winget.Source_8wekyb3d8bbwe",
14 | "Name" : "winget",
15 | "Type" : "Microsoft.PreIndexed.Package"
16 | }
17 | }
18 | ]
19 | }
20 |
21 | const generateWingetImport = async (apps) => {
22 | const packages = [];
23 |
24 | await apps.map(app => {
25 | const individualPackage = {
26 | "Id": app._id,
27 | "Version": app.selectedVersion
28 | }
29 |
30 | packages.push(individualPackage);
31 | })
32 |
33 | wingetImportSchema.Sources[0].Packages = packages;
34 |
35 | return wingetImportSchema;
36 | }
37 |
38 | module.exports = generateWingetImport;
--------------------------------------------------------------------------------
/utils/helpers.js:
--------------------------------------------------------------------------------
1 | export let shuffleArray = (a) => {
2 | for (let i = a.length - 1; i > 0; i--) {
3 | const j = Math.floor(Math.random() * (i + 1));
4 | [a[i], a[j]] = [a[j], a[i]];
5 | }
6 | return a;
7 | }
8 |
9 | export let checkTheme = () => {
10 | let isLight = false;
11 |
12 | const themeChecker = window.matchMedia('(prefers-color-scheme: light)');
13 |
14 | // if user doesn't have preference, we check their browser theme
15 | if (!localStorage.getItem("wiTheme")) {
16 | if (themeChecker.matches) {
17 | document.body.classList.add("light");
18 | localStorage.setItem("wiTheme", "light")
19 | isLight = true;
20 | } else {
21 | document.body.classList.add("dark");
22 | localStorage.setItem("wiTheme", "dark")
23 | isLight = false;
24 | }
25 | } else{
26 | isLight = localStorage.getItem("wiTheme") === "light" ? true : false;
27 |
28 | if(isLight){
29 | document.body.classList.add("light");
30 | } else{
31 | document.body.classList.add("dark");
32 | }
33 | }
34 |
35 | // listen to browser theme changes
36 | if (window.matchMedia) {
37 | themeChecker.addListener(() => {
38 | if (themeChecker.matches) {
39 | localStorage.setItem("wiTheme", "light")
40 | document.body.classList.replace("dark", "light");
41 | isLight = true;
42 | } else {
43 | localStorage.setItem("wiTheme", "dark")
44 | document.body.classList.replace("light", "dark");
45 | isLight = false
46 | }
47 | })
48 | }
49 |
50 | return isLight;
51 | }
52 |
53 | export let compareVersion = (v1, v2) => {
54 | if (typeof v1 !== "string") return false;
55 | if (typeof v2 !== "string") return false;
56 |
57 | // deal with version strings like 78.0b1
58 | if (v1.match(/\d([A-Za-z])\d/) && v2.match(/\d([A-Za-z])\d/)){
59 | if(v1 > v2) return 1;
60 | if(v1 < v2) return -1;
61 | if(v1 === v2) return 0;
62 | }
63 |
64 | v1 = v1.split(".");
65 | v2 = v2.split(".");
66 | const k = Math.min(v1.length, v2.length);
67 | for (let i = 0; i < k; ++i) {
68 | v1[i] = parseInt(v1[i], 10);
69 | v2[i] = parseInt(v2[i], 10);
70 | if (v1[i] > v2[i]) return 1;
71 | if (v1[i] < v2[i]) return -1;
72 | }
73 | return v1.length === v2.length ? 0 : v1.length < v2.length ? -1 : 1;
74 | }
75 |
76 | export let timeAgo = (isoDate) => {
77 | let date = new Date(isoDate);
78 |
79 | let seconds = Math.floor((Date.now() - date) / 1000);
80 | let unit = "second";
81 | let direction = "ago";
82 |
83 | let value = seconds;
84 | if (seconds >= 31536000) {
85 | value = Math.floor(seconds / 31536000);
86 | unit = "year";
87 | } else if (seconds >= 86400) {
88 | value = Math.floor(seconds / 86400);
89 | unit = "day";
90 | } else if (seconds >= 3600) {
91 | value = Math.floor(seconds / 3600);
92 | unit = "hour";
93 | } else if (seconds >= 60) {
94 | value = Math.floor(seconds / 60);
95 | unit = "minute";
96 | }
97 |
98 | // if it's more than 30 days, we just return the actual date
99 | let months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
100 |
101 | if(value > 30 && unit === "day"){
102 | return `${months[date.getMonth()]} ${date.getDate()}, ${date.getFullYear()}`;
103 | }
104 |
105 | if (value != 1) unit = unit + "s";
106 | return value + " " + unit + " " + direction;
107 | }
--------------------------------------------------------------------------------